130

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

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) 压缩堆(= 移动对象 = 更改地址)。

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

11 回答 11

76

仅限 .NET 4 及更高版本

好消息,大家!

这项工作的完美工具内置于 .NET 4 中,名为ConditionalWeakTable<TKey, TValue>. 这节课:

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

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

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

于 2009-04-15T09:44:40.747 回答
46

检查了ObjectIDGenerator类?这可以完成您尝试做的事情,以及 Marc Gravell 所描述的事情。

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

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

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

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

于 2009-04-15T10:58:53.487 回答
39

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

于 2009-04-15T09:44:48.890 回答
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() 或简单的整数以实现最快的访问。

于 2009-04-15T11:31:25.887 回答
7

这个方法怎么样:

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

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

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

问题?

于 2011-10-22T22:03:41.707 回答
5

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

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

于 2011-07-22T09:42:24.090 回答
4

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

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

于 2009-04-15T09:44:52.557 回答
2

我知道这已得到解答,但至少要注意您可以使用:

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

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

于 2010-10-20T18:32:47.703 回答
2

如果您在自己的代码中为特定用途编写模块,majkinetor 的方法 可能有效。但也有一些问题。

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

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

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

哈希码是一个数值,用于在基于哈希的集合中插入和标识对象,例如 Dictionary<TKey, TValue> 类、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();
    }
}

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

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

于 2015-07-07T12:19:24.610 回答
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不直接暴露的原因-您不想弄乱事物并因此导致内存泄漏。下一个最好的事情是 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
    }
于 2014-01-07T10:21:23.940 回答