web-dev-qa-db-fra.com

Comment puis-je obtenir un identifiant me permettant de distinguer les différentes instances d'une classe?

Imaginez que j'ai une seule classe, avec deux instances:

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

MyClass a une méthode PrintUniqueInstanceID:

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

Idéalement, le résultat serait quelque chose comme:

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

Alors, qu'est-ce que j'insérerais dans "[what goes here???]", ci-dessus, qui affiche un numéro unique pour chaque instance unique de la classe?

Idées

  1. Peut-être lancer "ceci" sur un pointeur int et l'utiliser?
  2. Utiliser GCHandle en quelque sorte?
  3. Accéder à une propriété de "this", dans la méthode, pour l'identifier de manière unique?

(facultatif) Informations de base pour les experts

La raison pour laquelle j'ai besoin de cela est que j'utilise AOP et PostSharp pour détecter automatiquement les problèmes de threading. J'ai besoin de rechercher chaque instance unique de la classe dans un dictionnaire, afin de vérifier que plusieurs threads n'accèdent pas à la même instance unique d'une classe (c'est correct s'il existe un seul thread par instance de classe).

Mise à jour

Comme d'autres l'ont fait remarquer, je devrais mentionner que je ne peux toucher aucune des classes existantes dans le projet 30 000 lignes. PrintUniqueInstanceID, ci-dessus, est un aspect (voir PostSharp ) ajouté à la classe de niveau supérieur, hérité de chaque classe du projet entier et exécuté sur chaque entrée de méthode du projet entier.

Une fois que j'ai vérifié que tout est thread-safe, je supprime l'aspect pour restaurer les performances.

13
Contango

Utilisez la classe ObjectIDGenerator:

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

Citation:

Les ID sont uniques pour la vie de l'instance ObjectIDGenerator.

En utilisant une table de hachage, ObjectIDGenerator conserve quel ID est affecté à quel objet. Les références d'objet, qui identifient de manière unique chaque objet, sont des adresses du segment de mémoire collecté à la poubelle au moment de l'exécution. Les valeurs de référence d'objet peuvent changer pendant la sérialisation, mais la table est mise à jour automatiquement pour que les informations soient correctes.

Les ID d'objet sont des nombres 64 bits. L'allocation commence à un, de sorte que zéro n'est jamais un ID d'objet valide. Un formateur peut choisir une valeur nulle pour représenter une référence d'objet dont la valeur est null.

Mise à jour

C'est le code qui résout le problème. Dans la classe d'aspect, utilisez les éléments suivants:

public static ObjectIDGenerator ObjectIDGen = new ObjectIDGenerator();

puis:

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

Mise à jour

Je pensais publier le code sur lequel est basé tout ce message. Ce code permet de détecter les points chauds de sécurité des threads sur l'ensemble d'un projet en déclenchant des avertissements si plusieurs threads accèdent à la même instance d'une classe.

Utile si vous avez 30 000 lignes de code existant et que vous souhaitez ajouter une vérification plus formelle de la sécurité des threads (chose extrêmement difficile à faire normalement). Cela a un impact sur les performances d'exécution, vous pouvez donc le supprimer après l'avoir exécuté pendant quelques jours en mode débogage.

Pour l'utiliser, ajoutez PostSharp + cette classe à votre projet, puis ajoutez un aspect "[MyThreadSafety]" à n'importe quelle classe. PostSharp insérera le code dans "OnEntry" avant chaque appel de méthode. L'aspect se propage à toutes les sous-classes et sous-méthodes, vous pouvez donc ajouter des contrôles de sécurité des threads à un projet entier avec une seule ligne de code.

Pour un autre exemple de cette technique en action, voir un exemple conçu pour ajouter facilement du cache aux appels de méthodes .

    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;
            }
        }
    }

Je peux fournir le projet de démonstration complet sur demande.

3
Contango

Ajoutez une propriété Guid à votre classe, puis dans le constructeur de la classe, affectez-la à 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); 
    } 
}
13
mgnoonan

Réponse révisée

Sur la base des informations supplémentaires dont nous disposons maintenant, je pense que vous pouvez résoudre votre problème très facilement en utilisant ConditionalWeakTable (qui est uniquement à partir de .NET 4 ).

  • il peut être utilisé pour associer des données arbitraires à des instances d'objet géré
  • cela fait not garde les objets "en vie" juste parce qu'ils ont été entrés comme clés dans la table
  • il utilise égalité de référence pour déterminer l'identité de l'objet; déplacement, auteurs de la classe ne peut pas modifier ce comportement (pratique car vous n'êtes pas l'auteur de toutes les classes sur la terre)
  • il peut être peuplé à la volée

Vous pouvez donc créer une telle table globale dans votre classe "manager" et associer chaque objet à une variable long, une Guid ou à tout autre élément de votre choix¹. Chaque fois que votre responsable rencontre un objet, il peut obtenir son identifiant associé (si vous l'avez déjà vu) ou l'ajouter à la table et l'associer à un nouvel identifiant créé sur-le-champ.

¹ En réalité, les valeurs de la table doivent être d'un type de référence, vous ne pouvez donc pas utiliser, par exemple. long comme type de valeur directement. Cependant, une solution simple consiste à utiliser object à la place et à laisser les valeurs long être encadrées.

Réponse originale

N'est-ce pas un exemple d'utilisation de base pour les membres static?

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

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

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

Bien sûr, vous devez faire attention à la gamme d'identificateurs afin de ne pas commencer à les réutiliser si des milliards d'instances sont créées, mais c'est facile à résoudre.

12
Jon

Une solution active (non automatique) lors du débogage consiste à cliquer avec le bouton droit de la souris sur une instance et à choisir "Créer un ID d'objet". Il va ajouter un {$1} à côté de votre nom d'instance et de votre classe.

Si plus tard, vous tombez sur une autre instance, il manquera cette marque {$1}.

1
JeromeJ

Pourrait potentiellement utiliser:

ClassName + MethodName + this.GetHashCode();

Bien que GetHashCode () ne garantisse pas une valeur unique, si elle est associée au nom de classe et au nom de méthode, la probabilité d'un conflit diminue.

Même en cas de conflit, le seul effet sera que cela générera plus d'avertissements dans les journaux, ce qui n'est pas grave.

0
Contango

Vous ne pouvez pas hériter de toutes vos classes d'une classe ou d'une interface de base et requérir l'implémentation d'une propriété UniqueID?

Une autre possibilité consiste à les envelopper dans une classe contenant une référence d'objet générique et l'ID unique, puis à les cataloguer à la demande. Nettoyer un tel catalogue d'assignations uniques pourrait être délicat.

0
Cade Roux