Informasi yang saya berikan disini bukanlah hal baru, saya hanya menambahkan ini untuk kelengkapan.
Ide kode ini cukup sederhana:
- Objek membutuhkan ID unik, yang tidak ada di sana secara default. Sebaliknya, kita harus mengandalkan hal terbaik berikutnya, yaitu
RuntimeHelpers.GetHashCode
memberi kita semacam ID unik
- Untuk memeriksa keunikan, ini berarti kita perlu menggunakan
object.ReferenceEquals
- Namun, kami masih ingin memiliki ID unik, jadi saya menambahkan
GUID
, yang menurut definisi unik.
- Karena saya tidak suka mengunci semuanya jika saya tidak perlu, saya tidak menggunakannya
ConditionalWeakTable
.
Gabungan, itu akan memberi Anda kode berikut:
public class UniqueIdMapper
{
private class ObjectEqualityComparer : IEqualityComparer<object>
{
public bool Equals(object x, object y)
{
return object.ReferenceEquals(x, y);
}
public int GetHashCode(object obj)
{
return RuntimeHelpers.GetHashCode(obj);
}
}
private Dictionary<object, Guid> dict = new Dictionary<object, Guid>(new ObjectEqualityComparer());
public Guid GetUniqueId(object o)
{
Guid id;
if (!dict.TryGetValue(o, out id))
{
id = Guid.NewGuid();
dict.Add(o, id);
}
return id;
}
}
Untuk menggunakannya, buat instance UniqueIdMapper
dan gunakan GUID yang dikembalikannya untuk objek.
Tambahan
Jadi, ada sedikit lagi yang terjadi di sini; izinkan saya menulis sedikit tentang ConditionalWeakTable
.
ConditionalWeakTable
melakukan beberapa hal. Yang paling penting adalah ia tidak peduli dengan pengumpul sampah, yaitu: objek yang Anda rujuk dalam tabel ini akan tetap dikumpulkan. Jika Anda mencari objek, pada dasarnya berfungsi sama dengan kamus di atas.
Penasaran bukan? Lagi pula, ketika sebuah objek dikumpulkan oleh GC, ia memeriksa apakah ada referensi ke objek tersebut, dan jika ada, ia mengumpulkannya. Jadi jika ada objek dariConditionalWeakTable
, mengapa objek yang direferensikan dikumpulkan?
ConditionalWeakTable
menggunakan trik kecil, yang juga digunakan oleh beberapa struktur .NET lainnya: alih-alih menyimpan referensi ke objek, ia sebenarnya menyimpan IntPtr. Karena itu bukan acuan yang sebenarnya, benda itu bisa dikoleksi.
Jadi, saat ini ada 2 masalah yang harus diatasi. Pertama, objek bisa dipindahkan di heap, jadi apa yang akan kita gunakan sebagai IntPtr? Dan kedua, bagaimana kita tahu bahwa objek memiliki referensi aktif?
- Objek dapat disematkan di heap, dan penunjuk aslinya dapat disimpan. Saat GC mengenai objek untuk dihapus, GC membongkar dan mengumpulkannya. Namun, itu berarti kita mendapatkan sumber daya yang disematkan, yang bukanlah ide yang baik jika Anda memiliki banyak objek (karena masalah fragmentasi memori). Ini mungkin bukan cara kerjanya.
- Saat GC memindahkan objek, GC memanggil kembali, yang kemudian dapat memperbarui referensi. Ini mungkin bagaimana penerapannya dilihat dari panggilan eksternal masuk
DependentHandle
- tapi saya yakin ini sedikit lebih canggih.
- Bukan penunjuk ke objek itu sendiri, tetapi penunjuk dalam daftar semua objek dari GC disimpan. IntPtr adalah indeks atau penunjuk dalam daftar ini. Daftar ini hanya berubah ketika sebuah objek mengubah generasi, di mana sebuah panggilan balik sederhana dapat memperbarui penunjuk. Jika Anda ingat cara kerja Mark & Sweep, ini lebih masuk akal. Tidak ada pin, dan penghapusan seperti sebelumnya. Saya percaya begitulah cara kerjanya
DependentHandle
.
Solusi terakhir ini memang mengharuskan runtime tidak menggunakan kembali bucket daftar sampai mereka dibebaskan secara eksplisit, dan juga mengharuskan semua objek diambil dengan panggilan ke runtime.
Jika kami menganggap mereka menggunakan solusi ini, kami juga dapat mengatasi masalah kedua. Algoritme Tandai & Sapu melacak objek mana yang telah dikumpulkan; segera setelah dikumpulkan, kami tahu saat ini. Setelah objek memeriksa apakah objek ada di sana, ia memanggil 'Gratis', yang menghapus penunjuk dan entri daftar. Benda itu benar-benar hilang.
Satu hal penting yang perlu diperhatikan pada saat ini adalah bahwa ada yang salah jika ConditionalWeakTable
diperbarui di beberapa utas dan jika tidak aman utas. Hasilnya akan menjadi kebocoran memori. Inilah sebabnya mengapa semua panggilan masuk ConditionalWeakTable
melakukan 'kunci' sederhana yang memastikan ini tidak terjadi.
Hal lain yang perlu diperhatikan adalah pembersihan entri harus dilakukan sesekali. Sementara objek sebenarnya akan dibersihkan oleh GC, entri tidak. Inilah mengapa ConditionalWeakTable
hanya tumbuh dalam ukuran. Setelah mencapai batas tertentu (ditentukan oleh peluang tabrakan dalam hash), ini memicu a Resize
, yang memeriksa apakah objek harus dibersihkan - jika ya, free
dipanggil dalam proses GC, menghapus IntPtr
pegangan.
Saya percaya ini juga mengapa DependentHandle
tidak diekspos secara langsung - Anda tidak ingin mengacaukan banyak hal dan akibatnya mendapatkan kebocoran memori. Hal terbaik berikutnya untuk itu adalah WeakReference
(yang juga menyimpan IntPtr
alih - alih objek) - tetapi sayangnya tidak menyertakan aspek 'ketergantungan'.
Yang tersisa adalah Anda bermain-main dengan mekanik, sehingga Anda dapat melihat ketergantungan dalam tindakan. Pastikan untuk memulainya beberapa kali dan lihat hasilnya:
class DependentObject
{
public class MyKey : IDisposable
{
public MyKey(bool iskey)
{
this.iskey = iskey;
}
private bool disposed = false;
private bool iskey;
public void Dispose()
{
if (!disposed)
{
disposed = true;
Console.WriteLine("Cleanup {0}", iskey);
}
}
~MyKey()
{
Dispose();
}
}
static void Main(string[] args)
{
var dep = new MyKey(true); // also try passing this to cwt.Add
ConditionalWeakTable<MyKey, MyKey> cwt = new ConditionalWeakTable<MyKey, MyKey>();
cwt.Add(new MyKey(true), dep); // try doing this 5 times f.ex.
GC.Collect(GC.MaxGeneration);
GC.WaitForFullGCComplete();
Console.WriteLine("Wait");
Console.ReadLine(); // Put a breakpoint here and inspect cwt to see that the IntPtr is still there
}