.NET唯一对象标识符

139

有没有一种方法可以获得实例的唯一标识符?

GetHashCode()对于指向同一实例的两个引用是相同的。但是,两个不同的实例可以(很容易地)获得相同的哈希码:

Hashtable hashCodesSeen = new Hashtable();
LinkedList<object> l = new LinkedList<object>();
int n = 0;
while (true)
{
    object o = new object();
    // Remember objects so that they don't get collected.
    // This does not make any difference though :(
    l.AddFirst(o);
    int hashCode = o.GetHashCode();
    n++;
    if (hashCodesSeen.ContainsKey(hashCode))
    {
        // Same hashCode seen twice for DIFFERENT objects (n is as low as 5322).
        Console.WriteLine("Hashcode seen twice: " + n + " (" + hashCode + ")");
        break;
    }
    hashCodesSeen.Add(hashCode, null);
}

我正在编写一个调试插件,需要获取某种在程序运行期间唯一的引用ID。

我已经成功获取了实例的内部地址,在垃圾回收器(GC)压缩堆之前是唯一的(移动对象=更改地址)。

Stack Overflow问题 Default implementation for Object.GetHashCode() 可能与此相关。

由于我是使用调试器API访问程序中的对象,因此这些对象不在我的控制之下。如果我能够控制这些对象,添加自己的唯一标识符将非常简单。

我想要为构建哈希表ID -> object获取唯一的ID,以便查找已经看到的对象。目前我是这样解决的:

Build a hashtable: 'hashCode' -> (list of objects with hash code == 'hashCode')
Find if object seen(o) {
    candidates = hashtable[o.GetHashCode()] // Objects with the same hashCode.
    If no candidates, the object is new
    If some candidates, compare their addresses to o.Address
        If no address is equal (the hash code was just a coincidence) -> o is new
        If some address equal, o already seen
}
11个回答

1

我在这里提供的信息并不新鲜,只是为了完整性而添加。

这段代码的思想非常简单:

  • 对象需要一个唯一的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在此列表中可以是索引或指针。只有当对象改变生成时,列表才会发生变化,此时简单的回调可以更新指针。如果您记得标记和清除的工作原理,这就更有意义了。没有固定,删除与之前一样。我相信这就是DependentHandle的工作方式。

最后的解决方案确实需要运行时不重用列表桶,直到它们被显式释放,并且还要求通过调用运行时检索所有对象。

如果我们假设他们使用了这个解决方案,我们也可以解决第二个问题。标记和清除算法跟踪哪些对象已被收集;一旦它被收集,我们就知道在这一点上。一旦对象检查是否存在,它就调用“Free”,这将删除指针和列表条目。对象真的不存在了。
此时需要注意的一件重要事情是,如果ConditionalWeakTable在多个线程中更新且不是线程安全的,则情况会变得非常糟糕。结果将是内存泄漏。这就是为什么ConditionalWeakTable中的所有调用都会执行简单的“lock”,以确保这种情况不会发生。
另一个需要注意的事情是,清理条目必须偶尔发生。虽然实际对象将由GC清理,但条目不会。这就是为什么ConditionalWeakTable只增长而不缩小。一旦它达到某个限制(由哈希中碰撞机会确定),它会触发Resize,该方法检查是否需要清理对象--如果需要,则在GC过程中调用free,从而删除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
    }

1
一个 ConditionalWeakTable 可能更好,因为它只会在对象存在引用时保留其表示。此外,我建议使用 Int64 而不是 GUID,因为它允许对象被赋予持久的 等级。这些东西在锁定场景中可能很有用(例如,如果所有需要获取多个锁的代码都按照某个定义的顺序执行,则可以避免死锁,但要使其起作用,必须 存在 定义的顺序)。 - supercat
@supercat 对于long,你的场景可能不同,例如在分布式系统中,使用GUID更有用。至于ConditionalWeakTable:你是对的;DependentHandle检查存活性(注意:仅当事物调整大小时!),这在这里可能很有用。但是,如果需要性能,则锁定可能成为问题,因此在这种情况下,使用这个可能会很有趣...老实说,我个人不喜欢ConditionalWeakTable的实现方式,这可能导致我倾向于使用简单的Dictionary - 即使你是正确的。 - atlaste
我一直对 ConditionalWeakTable 的实际工作原理感到好奇。它仅允许添加项的事实使我认为它旨在最小化与并发相关的开销,但我不知道它内部是如何工作的。我确实觉得有点奇怪的是,没有一个简单的 DependentHandle 包装器不使用表格,因为肯定有时需要确保一个对象在另一个对象的生命周期内保持活动状态,但后者没有空间引用第一个对象。 - supercat
@supercat 我会发布一个附录,介绍我认为它是如何工作的。 - atlaste
@supercat CWT负责处理瞬时句柄的“free”调用;GC负责对象垃圾回收(通常在句柄被释放之前,因为这只发生在调整大小调用期间)。我认为只有这些句柄是“pinned”的(实际上,我认为它们是某个运行时表中的指针,例如GC);对象本身不必被固定。顺便说一下,DependentHandle中可以找到这些free调用,在调整大小阶段调用它们。 - atlaste
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接