题
有没有办法获取实例的唯一标识符?
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
}
解决方案
在参考是的对象的唯一标识符。我不知道该转换成什么样的字符串等的参考值压实过程中会发生变化(如您所见)的任何方式,但每一次的值A将被改为值B,所以就为安全代码而言它仍然是一个唯一的ID。
如果所涉及的对象是你的控制之下,你可以创建一个使用的弱引用(避免防止垃圾收集)从引用您选择的ID(GUID,整型,等等)。这将添加一定量的开销和复杂性,但是。
其他提示
仅限 .NET 4 及更高版本
大家好消息!
这项工作的完美工具是在 .NET 4 中构建的,它被称为 ConditionalWeakTable<TKey, TValue>
. 。这个班:
- 可用于将任意数据与托管对象实例关联起来,就像字典一样(尽管它 是 不是字典)
- 不依赖于内存地址,因此不受 GC 压缩堆的影响
- 不会仅仅因为对象已作为键输入表中而使对象保持活动状态,因此可以使用它而无需使进程中的每个对象永远活动
- 使用引用相等性来确定对象身份;移动,类作者无法修改此行为,因此可以使用它 始终如一地 在任何类型的对象上
- 可以动态填充,因此不需要在对象构造函数中注入代码
签出 ObjectIDGenerator 类?这样做你试图做什么,以及马克·Gravell描述。
在ObjectIDGenerator跟踪先前识别的对象。当你问一个对象的ID,该ObjectIDGenerator知道是否返回现有的ID,或生成并记住一个新的ID。
在ID是用于ObjectIDGenerator实例的生命是唯一的。通常,ObjectIDGenerator生活持续只要创建它的格式化。对象ID才有意义仅在一个给定的序列化流,并且用于跟踪哪些对象序列化的对象图内具有给他人的引用。
使用哈希表,所述保留ObjectIDGenerator其ID被分配给哪个对象。对象的引用,它唯一地识别每个对象,是在运行垃圾收集堆地址。对象的参考值可以在序列化过程发生变化,但该表被自动更新,因此信息是正确的。
对象ID是64位的数字。分配从一开始,所以零从来都不是一个有效的对象ID。格式化器可以选择一个零值来表示的对象引用的值是空引用(Visual Basic中为Nothing)。
RuntimeHelpers.GetHashCode()
可有助于( MSDN 一>)。
您可以开发自己的东西在第二。例如:
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()或干脆整数的最快的访问。
这个怎么样方法:
设置在第一对象的字段的新值。如果在第二个对象相同的字段具有相同的值,它可能是相同的实例。否则,退出不同。
现在设置在所述第一对象中的字段,以不同的新的值。如果在第二对象相同的字段已经改变为不同的值,这是绝对相同的实例。
不要忘记在第一个对象设置现场发回它的退出原来的值。
问题?
有可能使在Visual Studio中唯一的对象标识符:在监视窗口中,用鼠标右键单击对象变量和选择的从上下文菜单使对象ID 的
不幸的是,这是一种手动步骤,并且我不相信该标识符可以通过代码访问。
您将不得不自己手动分配这样一个标识符, - 无论是实例内,或在外部
有关相关的数据库记录,主键可能是有用的(但你仍然可以得到一式两份)。另外,无论是使用Guid
,或保持自己的柜台,使用Interlocked.Increment
分配(并使其足够大,也不太可能溢出)。
我知道,这已经回答了,但它至少是有益的注意,你可以使用:
http://msdn.microsoft.com/en-我们/库/ system.object.referenceequals.aspx
这不会给你一个“唯一ID”直接,但在WeakReferences组合(和一个HashSet?)可以给你跟踪各种情况下的一个非常简单的方法。
我在这里提供的信息并不新鲜,我只是为了完整性而添加了这些信息。
这段代码的想法非常简单:
- 对象需要一个唯一的 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
做一个简单的“锁定”以确保这种情况不会发生。
另一件需要注意的事情是清理条目必须每隔一段时间进行一次。虽然实际对象将被 GC 清理,但条目却不会。这就是为什么 ConditionalWeakTable
只会变大。一旦达到一定的限制(由哈希中的碰撞机会确定),它就会触发 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
}
如果您正在自己的代码中编写用于特定用途的模块, 马基奈特法 可能 已经工作了。但也存在一些问题。
第一的, ,官方文档确实如此 不是 保证 GetHashCode()
返回一个唯一标识符(参见 Object.GetHashCode 方法 ()):
您不应该假设相等的哈希码意味着对象相等。
第二, ,假设您有非常少量的对象,以便 GetHashCode()
在大多数情况下都有效,此方法可以被某些类型覆盖。
例如,您正在使用一些 C 类,它会覆盖 GetHashCode()
始终返回 0。那么C的每个对象都会得到相同的哈希码。很遗憾, Dictionary
, HashTable
和其他一些关联容器将使用此方法:
哈希码是一个数值,用于在基于哈希的集合(例如 Dictionary<TKey, TValue> 类、Hashtable 类或从 DictionaryBase 类派生的类型)中插入和标识对象。GetHashCode 方法为需要快速检查对象相等性的算法提供此哈希代码。
所以,这种方法有很大的局限性。
和 更, ,如果你想建立一个通用库怎么办?您不仅无法修改所使用的类的源代码,而且它们的行为也是不可预测的。
我很感激 乔恩 和 西蒙 已经发布了他们的答案,我将在下面发布一个代码示例和关于性能的建议。
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
实现速度比之前快 1.8 倍 ObjectIDGenerator
执行。