Информация, которую я здесь привожу, не нова, я просто добавил ее для полноты.
Идея этого кода довольно проста:
- Объектам нужен уникальный идентификатор, которого нет по умолчанию. Вместо этого мы должны полагаться на следующую лучшую вещь, а именно
RuntimeHelpers.GetHashCode
на получение своего рода уникального идентификатора.
- Это означает, что для проверки уникальности нам нужно использовать
object.ReferenceEquals
- Однако мы все равно хотели бы иметь уникальный идентификатор, поэтому я добавил
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. Поскольку это не настоящая ссылка, объект можно собрать.
Итак, на данный момент необходимо решить 2 проблемы. Во-первых, объекты можно перемещать в куче, так что мы будем использовать в качестве IntPtr? И во-вторых, как мы узнаем, что у объектов есть активная ссылка?
- Объект можно закрепить в куче и сохранить его реальный указатель. Когда сборщик мусора попадает в объект для удаления, он открепляет его и забирает. Однако это будет означать, что мы получим закрепленный ресурс, что не является хорошей идеей, если у вас много объектов (из-за проблем фрагментации памяти). Вероятно, это не так.
- Когда GC перемещает объект, он выполняет обратный вызов, который затем может обновить ссылки. Судя по внешним вызовам, это может быть именно так,
DependentHandle
но я считаю, что это немного сложнее.
- Сохраняется не указатель на сам объект, а указатель в списке всех объектов из GC. IntPtr - это либо индекс, либо указатель в этом списке. Список изменяется только тогда, когда объект меняет поколения, и в этот момент простой обратный вызов может обновить указатели. Если вы помните, как работает Mark & Sweep, это имеет больше смысла. Прикрепления нет, а удаление как было раньше. Я считаю, что вот как это работает
DependentHandle
.
Это последнее решение требует, чтобы среда выполнения не использовала повторно сегменты списка до тех пор, пока они не будут явно освобождены, а также требует, чтобы все объекты извлекались с помощью вызова среды выполнения.
Если мы предположим, что они используют это решение, мы также сможем решить вторую проблему. Алгоритм Mark & Sweep отслеживает, какие объекты были собраны; как только он был собран, мы знаем об этом. Как только объект проверяет, есть ли объект, он вызывает «Free», который удаляет указатель и запись в списке. Объект действительно исчез.
Здесь следует отметить одну важную вещь: все идет ужасно неправильно, если ConditionalWeakTable
обновление выполняется в нескольких потоках и не является потокобезопасным. Результатом будет утечка памяти. Вот почему все вызовы ConditionalWeakTable
выполняются с помощью простой «блокировки», которая гарантирует, что этого не произойдет.
Также следует отметить, что очистка записей должна происходить время от времени. Хотя фактические объекты будут очищены сборщиком мусора, записи - нет. Вот почему 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
}