.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个回答

77

.NET 4及以上版本适用

大家好,有好消息!

这个工作的完美工具已经内置于.NET 4中,它叫做ConditionalWeakTable<TKey, TValue>。这个类:

  • 可以像字典一样关联处理过程中对象实例和任意数据(尽管它并非字典)
  • 不依赖内存地址,因此不会受到GC压缩堆的影响
  • 不会使通过键值进入表格的对象保持活动状态,因此可以在不让处理过程中的每个对象永远存活的情况下使用
  • 使用引用相等性来确定对象身份;此外,类作者无法修改此行为,因此可以在任何类型的对象上一致地使用
  • 可以动态填充,因此不需要在对象构造函数中插入代码

10
为了完整起见:ConditionalWeakTable 依赖于 RuntimeHelpers.GetHashCodeobject.ReferenceEquals 来进行其内部操作。这种行为与构建一个使用这两种方法的 IEqualityComparer<T> 相同。如果您需要性能,我建议您实际上这样做,因为 ConditionalWeakTable 在其所有操作周围都有锁定以使其线程安全。 - atlaste
1
@StefandeBruijn:ConditionalWeakTable 保存对每个 Value 的引用,这些引用只有与相应的 Key 在其他地方保持的引用一样强。当一个对象是 ConditionalWeakTable 中唯一存在的引用,并且其键不存在时,该对象将自动停止存在。 - supercat

51

你看过ObjectIDGenerator类吗?它可以实现你正在尝试的功能,与Marc Gravell所描述的相同。

ObjectIDGenerator跟踪先前确定的对象。当您请求一个对象的ID时,ObjectIDGenerator知道是返回现有的ID还是生成并记住新的ID。

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

使用哈希表,ObjectIDGenerator保留分配给哪个对象的ID。对象引用唯一地标识每个对象,这些引用是运行时垃圾回收堆中的地址。对象引用值在序列化期间可能会发生更改,但表会自动更新以确保信息正确。

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


5
Reflector 告诉我 ObjectIDGenerator 是一个基于默认 GetHashCode 实现的哈希表(即它不使用用户重载)。 - Anton Tykhyy
当需要可打印的唯一标识符时,这可能是最好的解决方案。 - Roman Starkov
ObjectIDGenerator在手机上也没有实现。 - Anthony Wieser
我不完全理解ObjectIDGenerator在做什么,但它似乎可以运行,即使它使用了RuntimeHelpers.GetHashCode。我测试了两者,只有RuntimeHelpers.GetHashCode在我的情况下失败了。 - Daniel Bişar
+1 -- 在桌面上运行得非常流畅。 - Hot Licks
现在在.NET 8中已经过时了。有没有一个好的替代方案?只需使用RuntimeHelpers.GetHashCode()就可以了。 - undefined

48
参考文献是对象的唯一标识符。我不知道有任何将其转换为字符串等内容的方法。参考文献的值将在压缩期间更改(如您所见),但是每个先前的值A都将更改为值B,因此就安全代码而言,它仍然是唯一的ID。
如果涉及的对象在您的控制下,您可以使用弱引用(以避免阻止垃圾回收)从引用到您选择的ID(GUID、整数等)创建映射。然而,这将增加一定量的开销和复杂性。

1
我猜查找时你需要遍历所有跟踪的引用:对于同一对象的WeakReference并不相等,因此你不能做太多其他的事情。 - Roman Starkov
1
如果每个对象都分配一个唯一的64位ID,并且这些ID是按顺序发放的,那么这可能会有一些用处。我不确定这种用处是否能够证明成本的价值,但如果比较两个不同的不可变对象并发现它们相等,则这种方法可能会有所帮助;如果尽可能地将对新对象的引用覆盖为对旧对象的引用,则可以避免对相同但不同的对象存在许多冗余引用。 - supercat
1
“标识符。”我认为这个词并不是你所想的意思。 - Slipp D. Thompson
5
不,它仍然是一对一的关系。只有一个引用值可以引用任何给定的对象。该值可能在内存中出现多次(例如作为多个变量的值),但它仍然是单个值。这就像房屋地址:我可以在多张纸上写下我的家庭地址,但那仍然是我的房子的标识符。任何两个非相同的引用值必须引用不同的对象——至少在C#中是这样。 - Jon Skeet
1
@supercat:我认为我们对“封装身份”的理解可能存在差异,但我认为我们也可能无法帮助任何人进一步了解这个问题 :) 如果我们有机会见面,这只是我们应该详细讨论的话题之一... - Jon Skeet
显示剩余15条评论

42

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


2
这可能会有所帮助,但代价是需要分配同步块,这并不是免费的。不过这是个好主意 - 我给你点赞。 - Jon Skeet
1
如果您不需要太多GCHandle(请参见下文),则可以使用GCHandle。 - Anton Tykhyy
47
一位备受尊敬的作者所著的一本.NET技术书中提到,RuntimeHelpers.GetHashCode()方法可以生成在一个应用程序域内唯一的代码,并且微软公司本来可以将该方法命名为GetUniqueObjectID。这是完全错误的。在测试中,我发现当我创建了约10,000个对象实例(例如WinForms文本框)时,通常会得到一个重复的哈希值,而且无论如何也达不到30,000个以上。依赖于所谓唯一性的代码在创建数量不到1/10的对象之后,在生产系统中导致间歇性崩溃。 - Jan Hettich
1
@JonSkeet:FYI,GetHashCode不会分配同步块。相反,对象头的同步块字段使用几个位来指示它是否表示同步块表中的偏移量,保存GetHashCode值或两者都不是。 - supercat
3
@supercat: 哈哈,我刚找到了一些证据,来自于2003年的.NET 1.0和1.1。看起来他们计划在.NET 2中进行更改:http://blogs.msdn.com/b/brada/archive/2003/09/30/50396.aspx - Jon Skeet
显示剩余3条评论

7

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

   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()生成的GUID,或者仅使用整数以获得更快速的访问。

2
如果您需要解决“Dispose”错误,这并不会有所帮助,因为这将防止任何类型的处理。 - Roman Starkov
1
这并不完全有效,因为字典使用相等性而不是身份,会折叠返回相同object.Equals值的对象。 - Anthony Wieser
2
这将保持对象的存活。 - Martin Lottering
2
@MartinLottering 如果他使用 ConditionalWeakTable<object, idType> 呢? - Demetris Leptos

7
这个方法如何:
在第一个对象中设置一个字段为新值。如果第二个对象中相同的字段具有相同的值,则它可能是相同的实例。否则,退出并表示不同。
现在将第一个对象中的字段设置为另一个新值。如果第二个对象中的相同字段已更改为不同的值,则它绝对是相同的实例。
不要忘记在退出时将第一个对象中的字段设置回其原始值。
存在问题吗?

5

在Visual Studio中可以创建一个唯一的对象标识符:在观察窗口中,右键单击对象变量,然后从上下文菜单中选择创建对象ID

不幸的是,这是一个手动步骤,我不认为可以通过代码访问该标识符。


这个功能在哪些版本的Visual Studio中有?例如,Express版本? - Peter Mortensen

3

您需要自己手动分配这样的标识符 - 可以在实例内部或外部执行。

对于与数据库相关的记录,主键可能很有用(但仍然可能会出现重复)。 或者,可以使用 Guid,或者保留自己的计数器,使用 Interlocked.Increment 进行分配(并确保它足够大,不太可能溢出)。


2

2
如果您正在为特定用途编写自己代码中的模块,majkinetor's method 可能有效。但是存在一些问题。 首先,官方文档并不保证GetHashCode()返回唯一标识符(请参见Object.GetHashCode Method ()):

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

其次,假设您拥有非常少量的对象,以便GetHashCode()在大多数情况下可以工作,那么某些类型可以重写此方法。例如,您正在使用某个类C,并将其覆盖为始终返回0的GetHashCode()。然后,C的每个对象都将获得相同的哈希码。不幸的是,DictionaryHashTable和一些其他关联容器将使用此方法:
哈希码是一个数字值,用于将对象插入和识别哈希基础集合中的对象,例如Dictionary类、Hashtable类或从DictionaryBase类派生的类型。GetHashCode方法为需要快速检查对象相等性的算法提供此哈希码。
因此,这种方法有很大的局限性。
而且更重要的是,如果您想构建一个通用库怎么办?您不仅无法修改所使用的类的源代码,而且它们的行为也是不可预测的。
我感谢JonSimon发布了他们的答案,我将在下面发布一个代码示例和性能建议。
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();
    }
}

在我的测试中,当在for循环中创建1000万个对象(比上面的代码多10倍)时,ObjectIDGenerator会抛出异常以投诉有太多的对象。此外,基准测试结果表明,ConditionalWeakTable实现比ObjectIDGenerator实现快1.8倍。

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