【问题标题】:How do I obtain an ID that allows me to tell difference instances of a class apart?如何获得一个 ID 以区分一个类的不同实例?
【发布时间】:2012-04-05 00:44:36
【问题描述】:

假设我有一个类,有两个实例:

MyClass a = new MyClass();
MyClass b = new MyClass();

MyClass 有一个方法 PrintUniqueInstanceID:

void PrintUniqueInstanceID()
{
  Console.Write("Unique ID for the *instance* of this class: {0}", 
      [what goes here???]
  );
}

理想情况下,输出应该是这样的:

Unique ID for the *instance* of this class: 23439434        // from a.PrintUniqueInstanceID
Unique ID for the *instance* of this class: 89654           // from b.PrintUniqueInstanceID

那么 - 我会在上面的“[what goes here???]”中插入什么,它会为类的每个唯一实例打印一个唯一编号?

想法

  1. 也许将“this”转换为 int 指针,然后使用它?
  2. 以某种方式使用 GCHandle?
  3. 在方法中访问“this”的属性以唯一标识它?

(可选)专家背景信息

我需要这个的原因是我正在使用 AOP 和 PostSharp 来自动检测线程问题。我需要在字典中查找类的每个唯一实例,以验证多个线程没有访问同一个类的唯一实例(如果每个类实例有一个线程,则可以)。

更新

正如其他人所指出的,我应该提到,我无法触及 30,000 行项目中的任何现有类。上面的 PrintUniqueInstanceID 是一个切面(参见PostSharp),它被添加到顶级类中,被整个项目中的每个类继承,并在整个项目中的每个方法入口上执行。

一旦我验证了所有内容都是线程安全的,我将删除方面以恢复性能。

【问题讨论】:

  • 据我所知,对象 a 和 b 只是它们在内存中的位置不同。因此,可行的是创建一个指向该对象的指针并查看其地址。我只是猜测,不确定这是否如我所说。
  • 您声明PrintUniqueInstanceID 是需要更改的MyClass成员,但您一直告诉回答者您无法更改“现有类” .那么它是什么?
  • @jmh_gr PrintUniqueInstanceID 是在类的每个方法的每个条目上自动执行的一个方面。因此,从某种意义上说,它是该类的成员——但它是由 PostSharp 在编译后步骤中添加的。我无法通过更改整个项目中的所有类来更改 30,000 行代码,但我可以很容易地为顶级类添加一个临时方面,它会自动被整个项目中的每个方法继承。一旦我确认所有内容都是线程安全的,我就可以删除它。
  • 您不能只将对象 ref 本身存储在字典中,还是出于其他原因需要唯一 id?

标签: c# .net visual-studio


【解决方案1】:

向您的类添加一个 Guid 属性,然后在该类的构造函数中将其分配给 NewGuid()。

public class MyClass
{
    public Guid InstanceID {get; private set;}
    // Other properties, etc.

    public MyClass()
    {
        this.InstanceID = Guid.NewGuid();
    }

    void PrintUniqueInstanceID() 
    {   
        Console.Write("Unique ID for the *instance* of this class: {0}", this.InstanceID); 
    } 
}

【讨论】:

  • 感谢您的回答!但是,这只有在我想更改整个 30,000 行项目中的每个类时才有效。我需要一些方法来唯一地标识 OnEntry 方面中的类实例,而不改变原始类本身。
  • 我会将二传手设为私有。为什么允许更改实例的 Guid?
  • @Gravitas 好的,您的问题未指定此要求。
  • @Gravitas 你不是已经在“Every class”中更改PrintUniqueInstanceID了吗?
  • @BrianRasmussen - 同意!已更新。
【解决方案2】:

修改后的答案

根据我们现在掌握的其他信息,我相信您可以使用ConditionalWeakTable仅从 .NET 4 开始)非常轻松地解决您的问题。

  • 它可用于将任意数据与托管对象实例相关联
  • 不会仅仅因为对象已作为键输入到表中而使对象保持“活动”
  • 它使用引用相等来确定对象身份;此外,课程作者无法修改此行为(很方便,因为您不是地球上每个课程的作者)
  • 可以即时填充

因此,您可以在“管理器”类中创建这样一个全局表,并将每个对象与longGuid 或您可能想要的任何其他对象相关联¹。每当您的经理遇到一个对象时,它可以获取其关联的 id(如果您以前见过它)或将其添加到表中并将其与当场创建的新 id 关联。

¹ 实际上表中的值必须是引用类型,所以你不能使用例如long 直接作为值类型。但是,一个简单的解决方法是改用 object 并将 long 值装箱。

原答案

这不是static成员的基本用法示例吗?

class Foo
{
    private static int instanceCounter;
    private readonly int instanceId;

    Foo()
    {
        this.instanceId = ++instanceCounter;
    }

    public int UniqueId
    {
        get { return this.instanceId; }
    }
}

当然,您必须注意标识符的范围,以免在创建数十亿个实例时开始重复使用它们,但这很容易解决。

【讨论】:

  • 感谢您的回答。但是,这只有在我想更改整个 30,000 行项目中的每个类时才有效。我需要一些方法来唯一地标识 OnEntry 方面中的类实例,而不改变原始类本身。我认为答案可能与使用 GCHandle 获取当前类的句柄有关。
  • @Gravitas:嗯,不涉及现有课程是您之前可以提到的一个重要细节。无论如何,AFAIK 没有办法在不破坏垃圾收集器性能的情况下做到这一点,这在我的书中意味着“不可行”。
  • 感谢您的反馈。我已经更新了这个问题。这方面的添加只是暂时的,以帮助验证整个项目的线程安全性。完成后我会删除它。
  • @Gravitas:.NET 4 似乎完全符合医生的规定——请参阅更新后的答案。
  • +1 修改后的答案,正是我的想法。一个小小的挑剔:CWT<K,V> 有一个(不必要的,imo)限制,即值(而不仅仅是键)必须是 ref 类型,因此您不能直接使用 longGuid . (没有理由你不能使用CWT<K,object> 而只是将值装箱。或者编写你自己的自定义包装类。)
【解决方案3】:

使用 ObjectIDGenerator 类:

http://msdn.microsoft.com/en-us/library/system.runtime.serialization.objectidgenerator.aspx

引用:

ID 在 ObjectIDGenerator 实例的生命周期内是唯一的。

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

对象 ID 是 64 位数字。分配从一开始,所以零是 绝不是有效的对象 ID。格式化程序可以选择一个零值来 表示一个值为 null 的对象引用。

更新

这是解决问题的代码。在方面类中,使用以下内容:

public static ObjectIDGenerator ObjectIDGen = new ObjectIDGenerator();

然后:

bool firstTime;
long classInstanceID = ObjectIDGenerator.GetId(args.Instance, out firstTime);

更新

我想我会发布整个帖子所基于的代码。此代码有助于检测整个项目中的线程安全热点,如果多个线程访问同一个类实例时会触发警告。

如果您有 30k 行现有代码,并且想要添加更正式的线程安全验证(通常很难做到这一点),这很有用。它确实会影响运行时性能,因此您可以在调试模式下运行几天后将其删除。

要使用,请将 PostSharp + 此类添加到您的项目中,然后将方面“[MyThreadSafety]”添加到任何类。 PostSharp 将在每个方法调用之前将代码插入“OnEntry”中。方面传播到所有子类和子方法,因此您只需一行代码即可将线程安全检查添加到整个项目。

有关此技术的另一个实际应用示例,请参阅example designed to easily add caching to method calls

    using System;
    using System.Diagnostics;
    using System.Reflection;
    using System.Runtime.CompilerServices;
    using System.Runtime.Serialization;
    using System.Text;
    using System.Threading;
    using MyLogType;
    using PostSharp.Aspects;
    using System.Collections.Concurrent;
    using PostSharp.Extensibility;

    namespace Demo
    {
        /// <summary>
        /// Example code based on the page from a Google search of:
        /// postsharp "Example: Tracing Method Execution"
        /// </summary>
        [Serializable]
        public sealed class MyThreadSafetyCheck : OnMethodBoundaryAspect
        {
            /// <summary>
            /// We need to be able to track if a different ThreadID is seen when executing a method within the *same* instance of a class. Its
            /// ok if we see different ThreadID values when accessing different instances of a class. In fact, creating one copy of a class per
            /// thread is a reliable method to fix threading issues in the first place.
            /// 
            /// Key: unique ID for every instance of every class.
            /// Value: LastThreadID, tracks the ID of the last thread which accessed the current instance of this class.
            /// </summary>
            public static ConcurrentDictionary<long, int> DetectThreadingIssues = new ConcurrentDictionary<long, int>();

            /// <summary>
            /// Allows us to generate a unique ID for each instance of every class that we see.
            /// </summary>
            public static ObjectIDGenerator ObjectIDGenerator = new ObjectIDGenerator();

            /// <summary>
            /// These fields are initialized at runtime. They do not need to be serialized.
            /// </summary>
            [NonSerialized]
            private string MethodName;

            [NonSerialized]
            private long LastTotalMilliseconds;

            /// <summary>
            /// Stopwatch which we can use to avoid swamping the log with too many messages for threading violations.
            /// </summary>
            [NonSerialized]
            private Stopwatch sw;

            /// <summary>
            /// Invoked only once at runtime from the static constructor of type declaring the target method. 
            /// </summary>
            /// <param name="method"></param>
            public override void RuntimeInitialize(MethodBase method)
            {
                if (method.DeclaringType != null)
                {
                    this.MethodName = method.DeclaringType.FullName + "." + method.Name;
                }

                this.sw = new Stopwatch();
                this.sw.Start();

                this.LastTotalMilliseconds = -1000000;
            }

            /// <summary>
            /// Invoked at runtime before that target method is invoked.
            /// </summary>
            /// <param name="args">Arguments to the function.</param>   
            public override void OnEntry(MethodExecutionArgs args)
            {
                if (args.Instance == null)
                {
                    return;
                }

                if (this.MethodName.Contains(".ctor"))
                {
                    // Ignore the thread that accesses the constructor.
                    // If we remove this check, then we get a false positive.
                    return;
                }

                bool firstTime;
                long classInstanceID = ObjectIDGenerator.GetId(args.Instance, out firstTime);

                if (firstTime)
                {
                    // This the first time we have called this, there is no LastThreadID. Return.
                    if (DetectThreadingIssues.TryAdd(classInstanceID, Thread.CurrentThread.ManagedThreadId) == false)
                    {
                        Console.Write(string.Format("{0}Error E20120320-1349. Could not add an initial key to the \"DetectThreadingIssues\" dictionary.\n",
                            MyLog.NPrefix()));
                    }
                    return;
                }

                int lastThreadID = DetectThreadingIssues[classInstanceID];

                // Check 1: Continue if this instance of the class was accessed by a different thread (which is definitely bad).
                if (lastThreadID != Thread.CurrentThread.ManagedThreadId)
                {
                    // Check 2: Are we printing more than one message per second?
                    if ((sw.ElapsedMilliseconds - this.LastTotalMilliseconds) > 1000)
                    {
                        Console.Write(string.Format("{0}Warning: ThreadID {1} then {2} accessed \"{3}\". To remove warning, manually check thread safety, then add \"[MyThreadSafetyCheck(AttributeExclude = true)]\".\n",
                            MyLog.NPrefix(), lastThreadID, Thread.CurrentThread.ManagedThreadId, this.MethodName));
                        this.LastTotalMilliseconds = sw.ElapsedMilliseconds;
                    }
                }

                // Update the value of "LastThreadID" for this particular instance of the class.
                DetectThreadingIssues[classInstanceID] = Thread.CurrentThread.ManagedThreadId;
            }
        }
    }

我可以按需提供完整的演示项目。

【讨论】:

  • 正如您的 cmets 中所解释的,所有这些都是为了检查“线程安全”。但在我看来,这本身就引入了线程安全问题:ObjectIDGenerator.GetId 不能保证是线程安全的! ConditionalWeakTable 来自 Jon's answer 是线程安全的,所以看起来更好。 (特别是因为它具有不阻止对象垃圾收集的额外好处。)
【解决方案4】:

调试时的主动(非自动)解决方案是右键单击实例并选择“生成对象 ID”。它会在您的实例名称和类旁边附加一个{$1}

如果稍后您偶然发现另一个实例,它将丢失 {$1} 标记。

【讨论】:

    【解决方案5】:

    您不能从基类或接口继承所有类并要求实现 UniqueID 属性?

    另一种可能性是将它们包装在一个包含通用对象引用和唯一 ID 的类中,然后以一种懒惰的方式对它们进行分类。清理这样一个独特的作业目录可能会很尴尬。

    【讨论】:

      【解决方案6】:

      可能使用:

      ClassName + MethodName + this.GetHashCode();
      

      虽然GetHashCode()不保证唯一值,但如果与类名和方法名配对,冲突的可能性就会降低。

      即使发生冲突,唯一的影响是它会在日志中生成更多警告,这没什么大不了的。

      【讨论】:

      • 决定反对该方法,因为 GetHashCode() 不能保证是唯一的,而 ObjectIDGenerator 是。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-10-26
      • 1970-01-01
      • 1970-01-01
      • 2019-12-11
      • 2021-07-18
      相关资源
      最近更新 更多