.NET唯一对象标识符


118

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

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作为参考,该ID在程序运行期间是唯一的。

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

堆栈溢出问题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
}

Answers:


42

参考对象的唯一标识符。我不知道将其转换为类似字符串之类的任何方式的方法。在压缩过程中,引用的值将更改(如您所见),但是到目前为止,每个先前的值A都将更改为值B。就安全代码而言,它仍然是唯一的ID。

如果所涉及的对象在您的控制之下,则可以使用弱引用(以避免垃圾回收)从您所选择的ID(GUID,整数等)的引用中创建映射。但是,这将增加一定量的开销和复杂性。


1
我猜对于查找,您必须遍历所跟踪的所有引用:对同一对象的WeakReference彼此不相等,因此您实际上无法做很多其他事情。
罗曼·斯塔科夫

1
为每个对象分配一个唯一的64位ID可能会有一些用处,特别是如果这些ID是顺序发布的。我不确定有用性是否会证明成本合理,但是如果人们比较两个不同的不可变对象并发现它们相等,那么这种事情可能会有所帮助。如果在可能的情况下用对较旧版本的引用覆盖对较新版本的引用,则可以避免对相同但不同的对象使用许多冗余引用。
supercat

1
“标识符。” 我认为这个词不代表您的意思。
Slipp D. Thompson

5
@ SlippD.Thompson:不,它仍然是一对一的关系。只有一个参考值引用任何给定的对象。该值可能在内存中出现多次(例如,作为多个变量的值),但仍然是单个值。这就像一个住所地址:我可以在很多纸上写下我的住所地址,但这仍然是我的房子的标识符。任何两个不同的引用值都必须引用不同的对象-至少在C#中。
乔恩·斯基特

1
@supercat:我认为我们对“被封装的身份”的理解可能有所不同-但我认为我们也可能没有帮助任何人超越已有的知识:)我们曾经亲自见面...
Jon Skeet 2014年

72

.NET 4及更高版本

大家好消息!

完美的工具内置于.NET 4中,它称为ConditionalWeakTable<TKey, TValue>。这节课:

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

5
仅出于完整性考虑:ConditionalWeakTable依靠RuntimeHelpers.GetHashCodeobject.ReferenceEquals完成其内部工作。行为与IEqualityComparer<T>使用这两种方法构建的行为相同。如果您需要性能,我实际上建议您这样做,因为ConditionalWeakTable它在所有操作上都有锁以使其线程安全。
atlaste'1

1
@StefandeBruijn:A ConditionalWeakTable持有对每个引用的引用,Value其强度与其他地方对相应引用的引用一样强KeyConditionalWeakTable当键存在时,一个对象在宇宙中任何地方拥有的唯一现存引用将自动不复存在。
2014年

41

签出了ObjectIDGenerator类?这就是您要尝试执行的操作,以及Marc Gravell所描述的。

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

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

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

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


5
Reflector告诉我ObjectIDGenerator是一个哈希表,它依赖于默认的GetHashCode实现(即,它不使用用户重载)。
Anton Tykhyy,2009年

当需要可打印的唯一ID时,可能是最佳解决方案。
罗曼·斯塔科夫

手机上也没有实现ObjectIDGenerator。
Anthony Wieser 2012年

我不完全了解ObjectIDGenerator在做什么,但即使使用RuntimeHelpers.GetHashCode,它也似乎可以正常工作。我都测试了,只有RuntimeHelpers.GetHashCode在我的情况下失败了。
DanielBişar2012年

+1-非常流畅(至少在桌面上)。
2014年

37

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


2
这可能会有所帮助,但要付出代价-IIRC使用基础对象.GetHashCode()需要分配一个同步块,这不是免费的。好主意-我+1。
乔恩·斯基特

谢谢,我不知道这种方法。但是,它也不产生唯一的哈希码(其行为与问题中的示例代码完全相同)。但是,如果用户重写哈希码来调用默认版本,它将很有用。
Martin Konicek 09年

1
如果不需要太多,可以使用GCHandle(请参见下文)。
2009年

42
备受推崇的作者在.NET上的书中指出,RuntimeHelpers.GetHashCode()将产生AppDomain内唯一的代码,并且Microsoft可以将其命名为GetUniqueObjectID方法。这是完全错误的。在测试中,我发现通常在创建10,000个对象实例(WinForms文本框)时会得到一个副本,并且永远不会超过30,000个。在创建不超过1/10个对象之后,依赖于假定的唯一性的代码在生产系统中导致间歇性崩溃。
Jan Hettich

3
@supercat:啊哈-从2003年才发现一些证据,它来自.NET 1.0和1.1。看起来他们打算为.NET 2进行更改:blogs.msdn.com/b/brada/archive/2003/09/30/50396.aspx
Jon Skeet 2012年

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作为唯一ID,例如System.Guid.NewGuid()或简单地选择整数以获得最快的访问权限。


2
如果您需要的是Disposebug,将无济于事,因为这会阻止任何形式的处理。
罗曼·斯塔科夫

1
这并不完全有效,因为字典使用相等性代替身份,折叠对象并返回相同的object.Equals
Anthony Wieser 2012年

1
但是,这将使对象保持活动状态。
Martin Lottering 2013年

1
@MartinLottering如果他使用ConditionalWeakTable <object,idType>怎么办?
Demetris Leptos

7

这个方法怎么样:

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

现在,将第一个对象中的字段设置为其他新值。如果第二个对象中的相同字段已更改为不同的值,则肯定是同一实例。

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

问题?


4

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

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


哪些版本的Visual Studio具有此功能?例如Express版本?
Peter Mortensen

3

您必须自己在实例内部或外部手动分配这样的标识符。

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



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是此列表中的索引或指针。仅当对象更改世代时,列表才会更改,此时简单的回调可以更新指针。如果您还记得Mark&Sweep的工作原理,那就更有意义了。没有固定,删除操作与以前一样。我相信这就是它的工作原理DependentHandle

最后一种解决方案确实要求运行时在显式释放它们之前不要重新使用列表存储桶,并且还要求通过对运行时的调用来检索所有对象。

如果我们假设他们使用此解决方案,我们还可以解决第二个问题。Mark&Sweep算法可跟踪收集到的对象。我们已经知道了这一点。一旦对象检查对象是否存在,它将调用“ Free”,这将删除指针和列表项。该对象真的消失了。

此时要注意的重要一件事是,如果ConditionalWeakTable在多个线程中进行更新并且如果它不是线程安全的,那么事情将发生严重错误。结果将是内存泄漏。这就是为什么所有来电ConditionalWeakTable都进行简单的“锁定”的原因,以确保不会发生这种情况。

需要注意的另一件事是清理条目必须不时进行。尽管实际对象将由GC清除,但条目不是。这就是为什么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
    }

1
A ConditionalWeakTable可能更好,因为它只会在对象存在引用时才保留对象的表示形式。另外,我建议an Int64可能比GUID更好,因为它将允许为对象赋予持久的等级。这样的事情在锁定场景中可能很有用(例如,如果所有需要获取多个锁的代码都按照某个定义的顺序这样做,则可以避免死锁,但是要使其工作必须一个定义的顺序)。
2014年

@supercat肯定longs;这取决于您的情况-在f.ex中。分布式系统,有时使用GUIDs 更为有用。至于ConditionalWeakTable:你是对的;DependentHandle检查活动性(注意:仅当事物调整大小时!),这在这里很有用。不过,如果您需要性能,则锁定可能会成为一个问题,因此在这种情况下使用它可能会很有趣...老实说,我个人不喜欢的实现ConditionalWeakTable,这可能导致我倾向于使用简单的Dictionary-甚至虽然你是对的。
atlaste'1

长期以来,我一直对ConditionalWeakTable实际运作方式感到好奇。它只允许添加项目的事实使我认为它旨在最大程度地减少与并发相关的开销,但是我不知道它在内部如何工作。我确实感到奇怪的是,没有一个简单的DependentHandle包装器不使用表,因为在某些情况下,一定要确保一个对象在另一个对象的生命周期中保持活动状态很重要,但是后一个对象没有可供参考的空间到第一个。
supercat 2014年

@supercat我将发布关于其工作原理的附录。
atlaste 2014年

ConditionalWeakTable已存储在表不允许的条目进行修改。因此,我认为可以使用内存屏障而不是锁来安全地实现它。唯一有问题的情况是两个线程试图同时添加相同的密钥;可以通过在添加一项后使“添加”方法执行内存屏障来解决此问题,然后进行扫描以确保恰好有一项具有该密钥。如果多个项目具有相同的密钥,则可以将其中一个标识为“第一”,因此可以消除其他项目。
supercat 2014年

0

如果您要用 自己的代码编写模块以用于特定用途,那么majkinetor的MIGHT 方法就可以了。但是有一些问题。

首先,官方文件并保证GetHashCode()收益的唯一标识符(见Object.GetHashCode()方法):

您不应假定相等的哈希码表示对象相等。

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

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

因此,这种方法有很大的局限性。

而且,如果你想建立一个通用的图书馆是什么?您不仅不能修改所使用类的源代码,而且它们的行为也是不可预测的。

我感谢乔恩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();
    }
}

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

同样,基准结果是该ConditionalWeakTable实施比ObjectIDGenerator实施快1.8倍。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.