Інформація, яку я даю тут, не нова, я просто додав це для повноти.
Ідея цього коду досить проста:
- Об'єктам потрібен унікальний ідентифікатор, якого немає за замовчуванням. Натомість ми маємо покладатися на наступне найкраще, що полягає
RuntimeHelpers.GetHashCode
в тому, щоб отримати нам свого роду унікальний ідентифікатор
- Щоб перевірити унікальність, це означає, що нам потрібно використовувати
object.ReferenceEquals
- Однак ми все-таки хотіли б мати унікальний ідентифікатор, тому я додав a
GUID
, який за визначенням унікальний.
- Тому що мені не подобається блокувати все, якщо мені не потрібно, я не використовую
ConditionalWeakTable
.
У поєднанні це дасть вам наступний код:
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;
}
}
Для його використання створіть екземпляр UniqueIdMapper
та використовуйте GUID, який він повертає для об'єктів.
Додаток
Отже, тут відбувається трохи більше; дозвольте мені трохи написати про це ConditionalWeakTable
.
ConditionalWeakTable
робить пару речей. Найголовніше, що це не хвилює збиральника сміття, тобто об’єкти, на які ви посилаєтесь у цій таблиці, збиратимуться незалежно. Якщо ви шукаєте об’єкт, він в основному працює так само, як і словник, наведений вище.
Цікаве ні? Зрештою, коли об’єкт збирається ГК, він перевіряє, чи є посилання на об’єкт, і якщо вони є, він збирає їх. Отже, якщо є об'єкт із ConditionalWeakTable
, чому тоді буде зібраний посилальний об'єкт?
ConditionalWeakTable
використовує невеликий трюк, який використовують і деякі інші .NET структури: замість того, щоб зберігати посилання на об’єкт, він фактично зберігає IntPtr. Оскільки це не справжня довідка, об’єкт можна зібрати.
Отже, на даний момент є дві проблеми, які потрібно вирішити. По-перше, об’єкти можна переміщувати по купі, так що ж ми будемо використовувати як IntPtr? По-друге, як ми знаємо, що об’єкти мають активну довідку?
- Об'єкт можна закріпити на купі, а його реальний покажчик може бути збережений. Коли GC потрапляє на об’єкт для видалення, він відкручує його та збирає. Однак це означає, що ми отримуємо закріплений ресурс, що не дуже добре, якщо у вас багато об’єктів (через проблеми з фрагментацією пам'яті). Це, мабуть, не так, як це працює.
- Коли GC переміщує об'єкт, він передзвонює, який потім може оновити посилання. Це може бути, як це реалізується, судячи з зовнішніх дзвінків,
DependentHandle
- але я вважаю, що це дещо складніше.
- Зберігається не вказівник на сам об’єкт, а вказівник у списку всіх об'єктів із GC. IntPtr - це або індекс, або вказівник у цьому списку. Список змінюється лише тоді, коли об'єкт змінює покоління, і тоді простий зворотний виклик може оновити покажчики. Якщо ви пам’ятаєте, як працює Mark & Sweep, це має більше сенсу. Немає фіксації, а видалення відбувається як раніше. Я вважаю, що так це працює
DependentHandle
.
Це останнє рішення вимагає, щоб час виконання не використовував повторно списки, поки вони не будуть явно звільнені, а також потрібно, щоб усі об'єкти були отримані за допомогою виклику до виконання.
Якщо припустити, що вони використовують це рішення, ми також можемо вирішити другу проблему. Алгоритм Позначення і зачистка відстежує, які об’єкти були зібрані; як тільки він буде зібраний, ми знаємо на даний момент. Як тільки об’єкт перевіряє, чи є об'єкт, він викликає "Безкоштовно", що видаляє вказівник та запис списку. Об’єкта справді немає.
Одне важливе, що слід зазначити в цьому моменті, - це те, що вони йдуть жахливо неправильно, якщо ConditionalWeakTable
вони оновлюються в декількох потоках і якщо це не є безпечним для потоків. Результатом буде витік пам'яті. Ось чому всі дзвінки ConditionalWeakTable
виконувати простий "замок", який забезпечує те, що цього не відбувається.
Ще одна річ, що слід зазначити, що чистка записів повинна відбуватися раз у раз. Хоча фактичні об’єкти будуть очищені GC, записи не є. Ось чому ConditionalWeakTable
тільки збільшується в розмірах. Як тільки він досягає певної межі (визначається випадковістю зіткнення в хеші), він спрацьовує a Resize
, який перевіряє, чи потрібно об'єкти очищати - якщо вони free
є, викликається в процесі GC, видаляючи IntPtr
ручку.
Я вважаю, що це також причина DependentHandle
, що не піддається впливу - ви не хочете возитися з речами і в результаті отримувати витік пам'яті. Наступне найкраще для цього - це WeakReference
(який також зберігає IntPtr
замість об'єкта) - але, на жаль, не включає аспект "залежності".
Залишилося вам пограти з механікою, щоб ви могли бачити залежність в дії. Не забудьте запустити його кілька разів і переглянути результати:
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
}