web-dev-qa-db-fra.com

Identifiant d'objet unique .NET

Y a-t-il un moyen d'obtenir un identifiant unique d'une instance?

GetHashCode() est identique pour les deux références pointant vers la même instance. Cependant, deux instances différentes peuvent (assez facilement) obtenir le même code de hachage:

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

J'écris un addin de débogage, et je dois obtenir une sorte d'identifiant pour une référence qui est unique pendant l'exécution du programme.

J'ai déjà réussi à obtenir une adresse interne de l'instance, qui est unique jusqu'à ce que le ramasse-miettes (GC) compacte le tas (= déplace les objets = modifie les adresses).

Stack Overflow question La mise en œuvre par défaut pour Object.GetHashCode () pourrait être liée.

Les objets ne sont pas sous mon contrôle car j'accède aux objets d'un programme en cours de débogage à l'aide de l'API du débogueur. Si je contrôlais les objets, ajouter mes propres identificateurs uniques serait trivial.

Je voulais un identifiant unique pour construire un identifiant de table de hachage -> objet, afin de pouvoir rechercher des objets déjà vus. Pour l'instant je l'ai résolu comme ceci:

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
}
104
Martin Konicek

La référence est l'identifiant unique de l'objet. Je ne connais aucun moyen de convertir cela en chaîne, etc. La valeur de la référence changera pendant le compactage (comme vous l'avez vu), mais chaque valeur précédente A sera remplacée par la valeur B, donc en tant que code sécurisé, il s'agit toujours d'un identifiant unique.

Si les objets concernés sont sous votre contrôle, vous pouvez créer un mappage à l'aide de faibles références (pour éviter d'empêcher le garbage collection) d'une référence à un ID de votre choix (GUID, entier, peu importe). Cela ajouterait toutefois une certaine quantité de frais généraux et de complexité.

38
Jon Skeet

.NET 4 et versions ultérieures uniquement

Bonnes nouvelles tout le monde!

L'outil idéal pour ce travail est construit dans .NET 4 et s'appelle ConditionalWeakTable<TKey, TValue> . Cette classe:

  • peut être utilisé pour associer des données arbitraires à des instances d'objet géré, un peu comme un dictionnaire (bien qu'il soit est pas un dictionnaire)
  • ne dépend pas des adresses de mémoire, il est donc à l'abri du compactage du tas par le CPG
  • ne garde pas les objets en vie juste parce qu'ils ont été entrés en tant que clés dans la table, de sorte qu'il peut être utilisé sans faire en sorte que chaque objet de votre processus vive à jamais
  • utilise l'égalité de référence pour déterminer l'identité de l'objet; les auteurs de classe ne peuvent pas modifier ce comportement afin qu'il puisse être utilisé toujours sur des objets de tout type
  • peut être rempli à la volée, il n'est donc pas nécessaire d'injecter du code dans les constructeurs d'objet
60
Jon

Vérifié le ObjectIDGenerator class? Cela correspond à ce que vous essayez de faire et à ce que décrit Marc Gravell.

ObjectIDGenerator assure le suivi des objets précédemment identifiés. Lorsque vous demandez l'ID d'un objet, ObjectIDGenerator sait s'il faut renvoyer l'ID existant ou générer et mémoriser un nouvel ID.

Les ID sont uniques pour la vie de l'instance ObjectIDGenerator. En règle générale, une vie ObjectIDGenerator dure aussi longtemps que le formateur qui l'a créé. Les identificateurs d'objet n'ont de signification que dans un flux sérialisé donné et sont utilisés pour suivre les objets référencés par d'autres dans le graphe d'objets sérialisés.

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 dans le segment de mémoire collecté par la mémoire d'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 identifiants d'objet sont des nombres de 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 une référence null (Nothing en Visual Basic).

37
sisve

RuntimeHelpers.GetHashCode() peut aider ( MSDN ).

34
Anton Gogolev

Vous pouvez développer votre propre chose en une seconde. Par exemple:

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

Vous pouvez choisir vous-même ce que vous souhaitez avoir comme identifiant unique, par exemple System.Guid.NewGuid () ou simplement un entier pour un accès plus rapide.

7
majkinetor

Que diriez-vous de cette méthode:

Définissez un champ dans le premier objet avec une nouvelle valeur. Si le même champ dans le deuxième objet a la même valeur, il s'agit probablement de la même instance. Sinon, quittez comme différent.

Définissez maintenant le champ du premier objet sur une nouvelle valeur différente. Si le même champ du deuxième objet a changé de valeur, il s'agit bien de la même instance.

N'oubliez pas de remettre le champ du premier objet à sa valeur d'origine à la sortie.

Problèmes?

6
Dawg

Il est possible de créer un identifiant d'objet unique dans Visual Studio: Dans la fenêtre de surveillance, cliquez avec le bouton droit de la souris sur la variable d'objet et choisissez Créer l'ID d'objet dans le menu contextuel.

Malheureusement, il s'agit d'une étape manuelle et je ne pense pas que l'identifiant soit accessible via un code.

4
Thomas Bratt

Vous devrez attribuer vous-même un tel identifiant, manuellement, soit à l'intérieur de l'instance, soit à l'extérieur.

Pour les enregistrements liés à une base de données, la clé primaire peut être utile (mais vous pouvez toujours obtenir des doublons). Vous pouvez également utiliser une variable Guid ou conserver votre propre compteur, en utilisant Interlocked.Increment (et rendez-le suffisamment grand pour qu'il ne risque pas de déborder).

3
Marc Gravell

Je sais que cela a été répondu, mais il est au moins utile de noter que vous pouvez utiliser:

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

Ce qui ne vous donnera pas directement un "identifiant unique", mais combiné à WeakReferences (et à un hashset?) Pourrait vous donner un moyen assez simple de suivre différentes instances.

2
Andrew Theken

Les informations que je donne ici ne sont pas nouvelles, je viens d’ajouter ceci par souci d’exhaustivité.

L'idée de ce code est assez simple:

  • Les objets ont besoin d'un identifiant unique, qui n'existe pas par défaut. Au lieu de cela, nous devons nous appuyer sur la meilleure solution suivante, à savoir RuntimeHelpers.GetHashCode, pour nous obtenir une sorte d'ID unique.
  • Pour vérifier l'unicité, cela implique l'utilisation de object.ReferenceEquals
  • Cependant, nous aimerions toujours avoir un identifiant unique. J'ai donc ajouté un GUID, qui est par définition unique.
  • Parce que je n'aime pas tout verrouiller si je n'ai pas à le faire, je n'utilise pas ConditionalWeakTable.

Combinés, cela vous donnera le code suivant:

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

Pour l'utiliser, créez une instance de UniqueIdMapper et utilisez les GUID renvoyés pour les objets.


Addenda

Donc, il y a un peu plus qui se passe ici; laissez-moi écrire un peu sur ConditionalWeakTable

ConditionalWeakTable fait plusieurs choses. La chose la plus importante est qu’il ne se soucie pas du ramasse-miettes, c’est-à-dire que les objets que vous référencez dans cette table seront collectés indépendamment. Si vous recherchez un objet, son fonctionnement est identique à celui du dictionnaire ci-dessus.

Curieux non? Après tout, lorsqu'un objet est collecté par le GC, il vérifie s'il existe des références à l'objet et, le cas échéant, il les collecte. Donc, s'il y a un objet de la ConditionalWeakTable, pourquoi l'objet référencé sera-t-il collecté alors?

ConditionalWeakTable utilise une petite astuce, que certaines autres structures .NET utilisent également: au lieu de stocker une référence à l'objet, elle stocke en fait un IntPtr. Parce que ce n'est pas une vraie référence, l'objet peut être collecté.

Donc, à ce stade, il y a 2 problèmes à résoudre. Premièrement, les objets peuvent être déplacés sur le tas, alors qu'utilisons-nous comme IntPtr? Et deuxièmement, comment savons-nous que les objets ont une référence active?

  • L'objet peut être épinglé sur le tas et son pointeur réel peut être stocké. Lorsque le GC frappe l'objet à supprimer, il le débloque et le collecte. Cependant, cela signifie que nous obtenons une ressource épinglée, ce qui n’est pas une bonne idée si vous avez beaucoup d’objets (en raison de problèmes de fragmentation de la mémoire). Ce n'est probablement pas comment cela fonctionne.
  • Lorsque le CPG déplace un objet, il le rappelle, qui peut ensuite mettre à jour les références. C'est peut-être la façon dont elle est mise en œuvre à en juger par les appels externes dans DependentHandle - mais je crois que c'est légèrement plus sophistiqué.
  • Ce n'est pas le pointeur sur l'objet lui-même, mais un pointeur dans la liste de tous les objets du GC est stocké. IntPtr est soit un index, soit un pointeur dans cette liste. La liste ne change que lorsqu'un objet change de génération. Un simple rappel peut alors mettre à jour les pointeurs. Si vous vous souvenez du fonctionnement de Mark & ​​Sweep, cela a plus de sens. Il n'y a pas d'épinglage et le retrait est comme avant. Je crois que c'est comme ça que ça marche dans DependentHandle.

Cette dernière solution nécessite que le moteur d'exécution ne réutilise pas les compartiments de la liste tant qu'ils ne sont pas explicitement libérés, et requiert également que tous les objets soient récupérés par un appel au moteur d'exécution.

Si nous supposons qu'ils utilisent cette solution, nous pouvons également résoudre le deuxième problème. L'algorithme Mark & ​​Sweep enregistre les objets collectés. dès qu'il a été collecté, nous le savons à ce stade. Une fois que l'objet vérifie si l'objet est là, il appelle 'Free', ce qui supprime le pointeur et l'entrée de la liste. L'objet est vraiment parti.

Une chose importante à noter à ce stade est que les choses tournent terriblement mal si ConditionalWeakTable est mis à jour dans plusieurs threads et s'il n'est pas thread-safe. Le résultat serait une fuite de mémoire. C'est pourquoi tous les appels dans ConditionalWeakTable font un simple "verrouillage" qui garantit que cela ne se produit pas.

Une autre chose à noter est que le nettoyage des entrées doit avoir lieu de temps en temps. Bien que les objets réels soient nettoyés par le CPG, les entrées ne le sont pas. C'est pourquoi ConditionalWeakTable ne fait que croître. Une fois qu'elle atteint une certaine limite (déterminée par le risque de collision dans le hachage), elle déclenche une Resize, qui vérifie si les objets doivent être nettoyés. Si c'est le cas, free est appelé dans le processus de GC, en supprimant le handle IntPtr.

Je crois que c’est aussi la raison pour laquelle DependentHandle n’est pas exposé directement. Vous ne voulez pas vous mêler des choses et obtenir une fuite de mémoire. La meilleure chose à faire pour cela est une WeakReference (qui stocke également une IntPtr au lieu d'un objet) - mais n'inclut malheureusement pas l'aspect «dépendance».

Il ne vous reste plus qu'à jouer avec les mécaniciens pour voir la dépendance en action. Assurez-vous de le démarrer plusieurs fois et observez les résultats:

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
atlaste

Si vous écrivez un module dans votre propre code pour un usage spécifique, la méthode de majkinetorPOURRAIT a fonctionné. Mais il y a quelques problèmes.

First, le document officiel ne PAS garantit que la fonction GetHashCode() renvoie un identificateur unique (voir Object.GetHashCode, méthode ()):

Vous ne devez pas supposer que des codes de hachage égaux impliquent une égalité d'objet.

Second, supposons que vous ayez une très petite quantité d'objets pour que GetHashCode() fonctionne dans la plupart des cas, cette méthode peut être remplacée par certains types. 
Par exemple, vous utilisez une classe C et remplace GetHashCode() pour toujours renvoyer 0. Chaque objet de C recevra le même code de hachage . Malheureusement, Dictionary, HashTable et certains autres conteneurs associatifs utiliseront ce méthode:

Un code de hachage est une valeur numérique utilisée pour insérer et identifier un objet dans une collection basée sur le hachage, telle que la classe Dictionary <TKey, TValue>, la classe Hashtable ou un type dérivé de la classe DictionaryBase. La méthode GetHashCode fournit ce code de hachage pour les algorithmes nécessitant des vérifications rapides de l'égalité des objets.

Donc, cette approche a de grandes limites.

Et encore plus, que se passe-t-il si vous voulez construire une bibliothèque à usage général? Non seulement vous ne pouvez pas modifier le code source des classes utilisées, mais leur comportement est également imprévisible.

J'apprécie que Jon et Simon aient posté leurs réponses, et je posterai un exemple de code et une suggestion sur les performances ci-dessous.

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

Dans mon test, la variable ObjectIDGenerator lève une exception pour se plaindre du nombre trop élevé d'objets créés lors de la création de 10 000 000 objets (10 fois plus que dans le code ci-dessus) dans la boucle for.

En outre, le résultat de référence est que l'implémentation ConditionalWeakTable est 1,8 fois plus rapide que l'implémentation ObjectIDGenerator.

0
Mr. Ree