.NET 唯一对象标识符

Posted

技术标签:

【中文标题】.NET 唯一对象标识符【英文标题】:.NET unique object identifier 【发布时间】:2010-10-19 13:17:18 【问题描述】:

有没有办法获取实例的唯一标识符?

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) 压缩堆之前是唯一的(= 移动对象 = 更改地址)。

堆栈溢出问题Default implementation for Object.GetHashCode() 可能是相关的。

对象不在我的控制之下,因为我正在使用调试器 API 访问正在调试的程序中的对象。如果我可以控制这些对象,添加我自己的唯一标识符将是微不足道的。

我想要用于构建哈希表 ID -> 对象的唯一 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

【问题讨论】:

【参考方案1】:

.NET 4 及更高版本

大家好!

完成这项工作的完美工具内置于 .NET 4 中,名为 ConditionalWeakTable&lt;TKey, TValue&gt;。这个类:

可用于将任意数据与托管对象实例关联起来,就像字典一样(尽管它不是字典) 不依赖于内存地址,因此不受 GC 压缩堆的影响 不会因为对象已作为键输入到表中而使对象保持活动状态,因此可以使用它而无需使进程中的每个对象都永久存在 使用引用相等来确定对象身份;此外,类作者无法修改此行为,因此它可以一致用于任何类型的对象 可以即时填充,因此不需要您在对象构造函数中注入代码

【讨论】:

为了完整性:ConditionalWeakTable 依赖于 RuntimeHelpers.GetHashCodeobject.ReferenceEquals 来完成其内部工作。该行为与构建使用这两种方法的IEqualityComparer&lt;T&gt; 相同。如果您需要性能,我实际上建议这样做,因为ConditionalWeakTable 对其所有操作都有一个锁定以使其线程安全。 @StefandeBruijn:ConditionalWeakTable 拥有对每个 Value 的引用,其强度与其他地方对相应 Key 的引用一样强。 ConditionalWeakTable 持有宇宙中任何地方唯一现存引用的对象将在密钥完成时自动不存在。【参考方案2】:

引用对象的唯一标识符。我不知道有任何方法可以将其转换为字符串等。引用的值在压缩期间会发生变化(如您所见),但到目前为止,每个先前的值 A 都将更改为值 B就安全代码而言,它仍然是唯一的 ID。

如果所涉及的对象在您的控制之下,您可以使用weak references(以避免垃圾收集)从您选择的 ID(GUID、整数等)的引用中创建映射。然而,这会增加一定的开销和复杂性。

【讨论】:

我猜对于查找,您必须遍历您跟踪的所有引用:对同一个对象的 WeakReference 并不相等,因此您真的无能为力。 为每个对象分配一个唯一的 64 位 ID 可能会有一些用处,特别是如果这些 ID 是按顺序发出的。我不确定有用性是否能证明成本合理,但如果比较两个不同的不可变对象并发现它们相等,这样的事情可能会有所帮助;如果在可能的情况下用对旧对象的引用覆盖对较新对象的引用,则可以避免对相同但不同的对象有许多冗余引用。 “标识符。” 我不认为这个词的意思和你想象的一样。 @SlippD.Thompson:不,它仍然是一对一的关系。只有一个引用值可以引用任何给定的对象。该值可能在内存中出现多次(例如,作为多个变量的值),但它仍然是单个值。这就像一个家庭地址:我可以在很多张纸上写下我的家庭地址,但这仍然是我房子的标识符。任何两个不相同的引用值必须引用不同的对象 - 至少在 C# 中。 @supercat:我认为我们对“身份被封装”的理解可能不同 - 但我认为我们也可能不会帮助任何人比我们已经拥有的更进一步:) 只是其中之一如果我们亲自见面,我们应该详细讨论的话题......【参考方案3】:

查看了ObjectIDGenerator 课程?这就是你想要做的事情,也是 Marc Gravell 所描述的。

ObjectIDGenerator 跟踪先前识别的对象。当您询问对象的 ID 时,ObjectIDGenerator 知道是返回现有 ID,还是生成并记住一个新 ID。

ID 在 ObjectIDGenerator 实例的生命周期内是唯一的。通常,ObjectIDGenerator 的生命周期与创建它的 Formatter 一样长。对象 ID 仅在给定的序列化流中有意义,并用于跟踪哪些对象引用了序列化对象图中的其他对象。

使用哈希表,ObjectIDGenerator 保留分配给哪个对象的 ID。唯一标识每个对象的对象引用是运行时垃圾收集堆中的地址。对象引用值在序列化过程中会发生变化,但表会自动更新,因此信息是正确的。

对象 ID 是 64 位数字。分配从 1 开始,因此 0 绝不是有效的对象 ID。格式化程序可以选择零值来表示其值为空引用的对象引用(在 Visual Basic 中为 Nothing)。

【讨论】:

Reflector 告诉我 ObjectIDGenerator 是一个依赖于默认 GetHashCode 实现的哈希表(即它不使用用户重载)。 当需要可打印的唯一 ID 时,这可能是最佳解决方案。 ObjectIDGenerator 也没有在手机上实现。 我不明白 ObjectIDGenerator 在做什么,但它似乎可以工作,即使它使用 RuntimeHelpers.GetHashCode。我测试了两者,只有 RuntimeHelpers.GetHashCode 在我的情况下失败。 +1 -- 工作得非常流畅(至少在桌面上)。【参考方案4】:

RuntimeHelpers.GetHashCode() 可能会有所帮助 (MSDN)。

【讨论】:

这可能会有所帮助,但要付出代价 - IIRC,使用基础 object.GetHashCode() 需要分配一个同步块,这不是免费的。好主意 - 来自我的 +1。 谢谢,我不知道这个方法。但是,它也不会产生唯一的哈希码(行为与问题中的示例代码完全相同)。但如果用户覆盖哈希码,调用默认版本将很有用。 如果您不需要太多 GCHandle,您可以使用它们(见下文)。 一位备受尊敬的作者关于 .NET 的书指出,RuntimeHelpers.GetHashCode() 将生成在 AppDomain 中唯一的代码,并且 Microsoft 可以将方法命名为 GetUniqueObjectID。这是完全错误的。在测试中,我发现当我创建 10,000 个对象实例(WinForms TextBox)时,我通常会得到一个副本,并且永远不会超过 30,000 个。依赖假定唯一性的代码在创建不超过 1/10 那么多对象后导致生产系统中的间歇性崩溃。 @supercat:啊哈——刚刚发现了一些证据,从 2003 年开始,来自 .NET 1.0 和 1.1。看起来他们正计划为 .NET 2 进行更改:blogs.msdn.com/b/brada/archive/2003/09/30/50396.aspx【参考方案5】:

您可以在一秒钟内开发自己的东西。例如:

   class Program
    
        static void Main(string[] args)
        
            var a = new object();
            var b = new object();
            Console.WriteLine("", a.GetId(), b.GetId());
        
    

    public static class MyExtensions
    
        //this dictionary should use weak key references
        static Dictionary<object, int> d = new Dictionary<object,int>();
        static int gid = 0;

        public static int GetId(this object o)
        
            if (d.ContainsKey(o)) return d[o];
            return d[o] = gid++;
        
       

您可以自行选择您希望拥有的唯一 ID,例如 System.Guid.NewGuid() 或简单的整数以实现最快的访问。

【讨论】:

如果您需要的是 Dispose 错误,这将无济于事,因为这会阻止任何形式的处置。 这并不完全有效,因为字典使用相等而不是标识,折叠返回相同值的对象 object.Equals 这将使对象保持活动状态。 @MartinLottering 如果他使用 ConditionalWeakTable 会怎样?【参考方案6】:

这个方法怎么样:

将第一个对象中的字段设置为新值。如果第二个对象中的相同字段具有相同的值,则它可能是相同的实例。否则,以不同的方式退出。

现在将第一个对象中的字段设置为不同的新值。如果第二个对象中的同一个字段变成了不同的值,那肯定是同一个实例。

不要忘记在退出时将第一个对象中的字段设置回其原始值。

有问题?

【讨论】:

【参考方案7】:

可以在 Visual Studio 中创建唯一的对象标识符:在监视窗口中,右键单击对象变量并从上下文菜单中选择 Make Object ID

很遗憾,这是一个手动步骤,我不相信可以通过代码访问标识符。

【讨论】:

什么版本的 Visual Studio 有这个功能?例如,Express 版本?【参考方案8】:

您必须自己手动分配此类标识符 - 在实例内部或外部。

对于与数据库相关的记录,主键可能很有用(但您仍然可以得到重复项)。或者,要么使用Guid,要么保留自己的计数器,使用Interlocked.Increment 进行分配(并使其足够大,以免溢出)。

【讨论】:

【参考方案9】:

我知道已经回答了这个问题,但至少要注意您可以使用:

http://msdn.microsoft.com/en-us/library/system.object.referenceequals.aspx

这不会直接为您提供“唯一 id”,但结合 WeakReferences(和哈希集?)可以为您提供一种跟踪各种实例的非常简单的方法。

【讨论】:

【参考方案10】:

如果您在自己的代码中为特定用途编写模块,majkinetor's method MIGHT 已经奏效。但是也有一些问题。

首先,官方文档确实保证GetHashCode()返回一个唯一标识符(参见Object.GetHashCode Method ()): p>

你不应该假设相等的哈希码意味着对象相等。

第二,假设您有非常少量的对象,因此GetHashCode() 在大多数情况下都可以工作,此方法可以被某些类型覆盖。 例如,您正在使用某个类 C,它会覆盖 GetHashCode() 以始终返回 0。然后 C 的每个对象都将获得相同的哈希码。 不幸的是,DictionaryHashTable 和其他一些关联容器会使用这种方法:

哈希码是一个数值,用于在基于哈希的集合中插入和标识对象,例如 Dictionary 类、Hashtable 类或从 DictionaryBase 类派生的类型。 GetHashCode 方法为需要快速检查对象相等性的算法提供此哈希码。

所以,这种方法有很大的局限性。

而且甚至,如果您想构建一个通用库怎么办? 您不仅无法修改使用的类的源代码,而且它们的行为也是不可预测的。

感谢Jon 和Simon 发布了他们的答案,我将在下面发布代码示例和性能建议。

using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Collections.Generic;


namespace ObjectSet

    public interface IObjectSet
    
        /// <summary> check the existence of an object. </summary>
        /// <returns> true if object is exist, false otherwise. </returns>
        bool IsExist(object obj);

        /// <summary> if the object is not in the set, add it in. else do nothing. </summary>
        /// <returns> true if successfully added, false otherwise. </returns>
        bool Add(object obj);
    

    public sealed class ObjectSetUsingConditionalWeakTable : IObjectSet
    
        /// <summary> unit test on object set. </summary>
        internal static void Main() 
            Stopwatch sw = new Stopwatch();
            sw.Start();
            ObjectSetUsingConditionalWeakTable objSet = new ObjectSetUsingConditionalWeakTable();
            for (int i = 0; i < 10000000; ++i) 
                object obj = new object();
                if (objSet.IsExist(obj))  Console.WriteLine("bug!!!"); 
                if (!objSet.Add(obj))  Console.WriteLine("bug!!!"); 
                if (!objSet.IsExist(obj))  Console.WriteLine("bug!!!"); 
            
            sw.Stop();
            Console.WriteLine(sw.ElapsedMilliseconds);
        


        public bool IsExist(object obj) 
            return objectSet.TryGetValue(obj, out tryGetValue_out0);
        

        public bool Add(object obj) 
            if (IsExist(obj)) 
                return false;
             else 
                objectSet.Add(obj, null);
                return true;
            
        

        /// <summary> internal representation of the set. (only use the key) </summary>
        private ConditionalWeakTable<object, object> objectSet = new ConditionalWeakTable<object, object>();

        /// <summary> used to fill the out parameter of ConditionalWeakTable.TryGetValue(). </summary>
        private static object tryGetValue_out0 = null;
    

    [Obsolete("It will crash if there are too many objects and ObjectSetUsingConditionalWeakTable get a better performance.")]
    public sealed class ObjectSetUsingObjectIDGenerator : IObjectSet
    
        /// <summary> unit test on object set. </summary>
        internal static void Main() 
            Stopwatch sw = new Stopwatch();
            sw.Start();
            ObjectSetUsingObjectIDGenerator objSet = new ObjectSetUsingObjectIDGenerator();
            for (int i = 0; i < 10000000; ++i) 
                object obj = new object();
                if (objSet.IsExist(obj))  Console.WriteLine("bug!!!"); 
                if (!objSet.Add(obj))  Console.WriteLine("bug!!!"); 
                if (!objSet.IsExist(obj))  Console.WriteLine("bug!!!"); 
            
            sw.Stop();
            Console.WriteLine(sw.ElapsedMilliseconds);
        


        public bool IsExist(object obj) 
            bool firstTime;
            idGenerator.HasId(obj, out firstTime);
            return !firstTime;
        

        public bool Add(object obj) 
            bool firstTime;
            idGenerator.GetId(obj, out firstTime);
            return firstTime;
        


        /// <summary> internal representation of the set. </summary>
        private ObjectIDGenerator idGenerator = new ObjectIDGenerator();
    

在我的测试中,ObjectIDGeneratorfor 循环中创建 10,000,000 个对象(比上面代码中的 10 倍)时会抛出异常来抱怨对象太多。

此外,基准测试结果是 ConditionalWeakTable 实现比 ObjectIDGenerator 实现快 1.8 倍。

【讨论】:

【参考方案11】:

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

这段代码的思路很简单:

对象需要唯一的 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 只会变大的原因。一旦达到一定的限制(由哈希中的碰撞机会确定),它就会触发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
    

【讨论】:

A ConditionalWeakTable 可能会更好,因为它只会在对象存在引用时保留对象的表示。另外,我建议Int64 可能比 GUID 更好,因为它允许对象被赋予持久的rank。这样的事情在锁定场景中可能很有用(例如,如果需要获取多个锁的所有代码都以某种定义的顺序执行,则可以避免死锁,但要使其工作必须定义的顺序)。 @supercat 当然是longs;这取决于您的情况 - 在 f.ex 中。分布式系统有时使用GUIDs 会更有用。至于ConditionalWeakTable:你说得对; DependentHandle 检查活动性(注意:仅当事物调整大小时!),这在这里很有用。尽管如此,如果您需要性能,锁定可能会成为一个问题,所以在这种情况下使用它可能会很有趣......老实说,我个人不喜欢ConditionalWeakTable 的实现,这可能导致我偏向于使用简单的Dictionary - 即使你是对的。 我一直很好奇ConditionalWeakTable 的实际工作原理。它只允许添加项目的事实让我认为它旨在最大限度地减少与并发相关的开销,但我不知道它在内部是如何工作的。我确实觉得奇怪的是,没有不使用表格的简单 DependentHandle 包装器,因为肯定有很多时候确保一个对象在另一个对象的生命周期内保持活动状态很重要,但后一个对象没有空间参考第一个。 @supercat 我会发布一个关于我认为它是如何工作的附录。 ConditionalWeakTable 不允许修改已存储在表中的条目。因此,我认为可以使用内存屏障而不是锁来安全地实现它。唯一有问题的情况是两个线程同时尝试添加相同的键;这可以通过在添加项目后让“add”方法执行内存屏障来解决,然后扫描以确保一个项目恰好具有该键。如果多个项目具有相同的密钥,其中一个将被识别为“第一”,因此可以消除其他项目。

以上是关于.NET 唯一对象标识符的主要内容,如果未能解决你的问题,请参考以下文章

从字符串转换为唯一标识符时 ASP.NET 转换失败

在运行时唯一标识对象的选项?

iOS核心数据对象的唯一标识符?

ASP.NET中GUID

serclet容器为每一个httpsession 对象分配一个唯一标识符,叫做啥

Android关于设备唯一标识符的获取,适配Android10