我在这里提供的信息并不是新的,我只是为了完整性而添加了这个。
这段代码的想法很简单:
- 对象需要唯一的 ID,默认情况下不存在。取而代之的是,我们必须依赖次优的事情,那就是
RuntimeHelpers.GetHashCode
为我们提供一种唯一的 ID
- 为了检查唯一性,这意味着我们需要使用
object.ReferenceEquals
- 但是,我们仍然希望有一个唯一的 ID,所以我添加了一个
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
做几件事。最重要的是它不关心垃圾收集器,即:你在这张表中引用的对象无论如何都会被收集。如果你查找一个对象,它基本上和上面的字典一样。
好奇不?毕竟,当一个对象被 GC 收集时,它会检查是否有对该对象的引用,如果有,它会收集它们。那么如果有一个来自 的对象,那么ConditionalWeakTable
为什么要收集引用的对象呢?
ConditionalWeakTable
使用了一个小技巧,其他一些 .NET 结构也使用:它实际上存储了一个 IntPtr,而不是存储对对象的引用。因为这不是真正的引用,所以可以收集对象。
所以,此时有两个问题需要解决。首先,对象可以在堆上移动,那么我们将使用什么作为 IntPtr?其次,我们怎么知道对象有一个活跃的引用?
- 对象可以固定在堆上,并且可以存储它的真实指针。当 GC 命中要移除的对象时,它会取消固定并收集它。但是,这意味着我们获得了固定资源,如果您有很多对象(由于内存碎片问题),这不是一个好主意。这可能不是它的工作方式。
- 当 GC 移动一个对象时,它会回调,然后可以更新引用。从外部调用来看,这可能是它的实现方式
DependentHandle
——但我相信它稍微复杂一些。
- 不是指向对象本身的指针,而是存储来自 GC 的所有对象列表中的指针。IntPtr 是此列表中的索引或指针。该列表仅在对象更改世代时更改,此时简单的回调可以更新指针。如果您还记得 Mark & Sweep 是如何工作的,那就更有意义了。没有固定,删除和以前一样。我相信这就是它在
DependentHandle
.
最后一个解决方案确实要求运行时在显式释放它们之前不重新使用列表存储桶,并且它还要求通过调用运行时检索所有对象。
如果我们假设他们使用这个解决方案,我们也可以解决第二个问题。Mark & Sweep 算法跟踪收集了哪些对象;一旦它被收集,我们就知道了。一旦对象检查对象是否存在,它就会调用“Free”,这会删除指针和列表条目。对象真的没了。
在这一点上要注意的一件重要的事情是,如果ConditionalWeakTable
在多个线程中更新并且它不是线程安全的,那么事情就会出现可怕的错误。结果将是内存泄漏。这就是为什么所有调用ConditionalWeakTable
都会执行简单的“锁定”以确保不会发生这种情况。
另一件需要注意的是,清理条目必须偶尔进行一次。虽然实际对象将被 GC 清理,但条目不会。这就是为什么ConditionalWeakTable
只会变大的原因。一旦达到一定的限制(由哈希中的碰撞机会确定),它就会触发 a Resize
,检查对象是否需要清理——如果需要清理,free
则在 GC 过程中调用,删除IntPtr
句柄。
我相信这也是为什么DependentHandle
不直接暴露的原因-您不想弄乱事物并因此导致内存泄漏。下一个最好的事情是 a 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
}