Unikalny identyfikator obiektu .NET

118

Czy istnieje sposób na uzyskanie unikalnego identyfikatora instancji?

GetHashCode()jest taki sam dla dwóch odniesień wskazujących na to samo wystąpienie. Jednak dwie różne instancje mogą (dość łatwo) uzyskać ten sam kod skrótu:

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

Piszę dodatek do debugowania i potrzebuję jakiegoś identyfikatora dla odniesienia, który jest unikalny podczas działania programu.

Udało mi się już uzyskać wewnętrzny ADRES instancji, który jest unikalny do momentu, gdy garbage collector (GC) kompaktuje stertę (= przenosi obiekty = zmienia adresy).

Pytanie o przepełnienie stosu Domyślna implementacja Object.GetHashCode () może być powiązana.

Obiekty nie są pod moją kontrolą, ponieważ uzyskuję dostęp do obiektów w debugowanym programie za pomocą interfejsu API debugera. Gdybym miał kontrolę nad obiektami, dodanie własnych unikalnych identyfikatorów byłoby trywialne.

Chciałem mieć unikalny identyfikator do budowania hashtable ID -> object, aby móc wyszukiwać już widziane obiekty. Na razie rozwiązałem to tak:

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
}
Martin Konicek
źródło

Odpowiedzi:

42

Odniesienie to unikalny identyfikator obiektu. Nie znam żadnego sposobu na przekształcenie tego w coś takiego jak ciąg itp. Wartość odniesienia zmieni się podczas kompaktowania (jak widzieliście), ale każda poprzednia wartość A zostanie zmieniona na wartość B, tak daleko jeśli chodzi o bezpieczny kod, nadal jest to unikalny identyfikator.

Jeśli kontrolujesz obiekty, których to dotyczy, możesz utworzyć mapowanie przy użyciu słabych referencji (aby uniknąć usuwania pamięci) z odniesienia do wybranego identyfikatora (GUID, liczba całkowita, cokolwiek). To jednak spowodowałoby pewne obciążenie i złożoność.

Jon Skeet
źródło
1
Wydaje mi się, że w przypadku wyszukiwań musiałbyś iterować po wszystkich śledzonych referencjach: Słabe odniesienia do tego samego obiektu nie są sobie równe, więc tak naprawdę nie możesz zrobić nic więcej.
Roman Starkov
1
Przypisanie każdemu obiektowi unikalnego 64-bitowego identyfikatora może być przydatne, zwłaszcza jeśli takie identyfikatory były nadawane sekwencyjnie. Nie jestem pewien, czy użyteczność uzasadniałaby koszty, ale taka rzecz może być pomocna, jeśli porównamy dwa różne niezmienne obiekty i uznamy, że są one równe; jeśli ktoś, o ile to możliwe, nadpisuje odniesienie do nowszego odniesieniem do starszego, można uniknąć wielu redundantnych odniesień do identycznych, ale odrębnych obiektów.
supercat
1
„Identyfikator”. Nie sądzę, że to słowo oznacza to, co myślisz, że oznacza.
Slipp D. Thompson
5
@ SlippD.Thompson: Nie, nadal jest to relacja 1 do 1. Istnieje tylko jedna wartość odniesienia, która odnosi się do dowolnego obiektu. Ta wartość może pojawiać się w pamięci wiele razy (np. Jako wartość wielu zmiennych), ale nadal jest to pojedyncza wartość. To jak adres domowy: mogę zapisać swój adres domowy na wielu kartkach papieru, ale to wciąż jest identyfikator mojego domu. Dowolne dwie nieidentyczne wartości odwołań muszą odnosić się do różnych obiektów - przynajmniej w C #.
Jon Skeet
1
@supercat: Myślę, że różnimy się w naszym rozumieniu pojęcia „hermetyzowane tożsamości” - ale myślę, że prawdopodobnie nie pomagamy nikomu pójść dalej niż to, co już mamy :) To tylko jeden z tematów, które powinniśmy szczegółowo omówić, jeśli kiedykolwiek spotykamy się osobiście ...
Jon Skeet,
72

Tylko .NET 4 i nowsze

Dobre wieści wszyscy!

Idealne narzędzie do tego zadania jest wbudowane w .NET 4 i nazywa się ConditionalWeakTable<TKey, TValue>. Ta klasa:

  • może służyć do kojarzenia dowolnych danych z instancjami zarządzanych obiektów, podobnie jak słownik (chociaż nie jest to słownik)
  • nie zależy od adresów pamięci, więc jest odporny na kompaktowanie sterty przez GC
  • nie utrzymuje obiektów przy życiu tylko dlatego, że zostały wprowadzone jako klucze do tabeli, więc można ich używać bez powodowania, że ​​każdy obiekt w twoim procesie żyje wiecznie
  • używa równości odwołań do określenia tożsamości obiektu; poruszając się, autorzy klas nie mogą modyfikować tego zachowania, więc może być używane konsekwentnie na obiektach dowolnego typu
  • może być wypełniany w locie, więc nie wymaga wstrzykiwania kodu do konstruktorów obiektów
Jon
źródło
5
Tylko dla kompletności: ConditionalWeakTablepolega na wewnętrznych funkcjach RuntimeHelpers.GetHashCodei object.ReferenceEqualswykonuje je. Zachowanie jest takie samo, jak przy tworzeniu obiektu, IEqualityComparer<T>który korzysta z tych dwóch metod. Jeśli potrzebujesz wydajności, sugeruję to zrobić, ponieważ ConditionalWeakTablema blokadę wokół wszystkich swoich operacji, aby zapewnić bezpieczeństwo wątków.
atlaste
1
@StefandeBruijn: A ConditionalWeakTablezawiera odniesienie do każdego z nich, Valuektóre jest tak silne, jak odniesienie utrzymywane w innym miejscu do odpowiedniego Key. Obiekt, do którego a ConditionalWeakTablezawiera jedyne istniejące odniesienie w dowolnym miejscu we wszechświecie, automatycznie przestanie istnieć, gdy klucz się pojawi.
supercat
41

Wyrejestrowany ObjectIDGenerator klasie? Robi to, co próbujesz zrobić i co opisuje Marc Gravell.

ObjectIDGenerator śledzi wcześniej zidentyfikowane obiekty. Kiedy pytasz o identyfikator obiektu, ObjectIDGenerator wie, czy zwrócić istniejący identyfikator, czy też wygenerować i zapamiętać nowy identyfikator.

Identyfikatory są unikalne przez cały okres istnienia instancji ObjectIDGenerator. Ogólnie rzecz biorąc, życie ObjectIDGenerator trwa tak długo, jak długo formatter, który go utworzył. Identyfikatory obiektów mają znaczenie tylko w danym zserializowanym strumieniu i są używane do śledzenia, które obiekty mają odniesienia do innych obiektów w serializowanym grafie obiektów.

Korzystając z tabeli skrótów, ObjectIDGenerator zachowuje identyfikator przypisany do danego obiektu. Odwołania do obiektów, które jednoznacznie identyfikują każdy obiekt, są adresami w stercie zbierania elementów bezużytecznych w czasie wykonywania. Wartości odwołań do obiektów mogą się zmieniać podczas serializacji, ale tabela jest aktualizowana automatycznie, więc informacje są poprawne.

Identyfikatory obiektów to liczby 64-bitowe. Alokacja zaczyna się od jedynki, więc zero nigdy nie jest prawidłowym identyfikatorem obiektu. Formater może wybrać wartość zero, aby reprezentować odwołanie do obiektu, którego wartość jest odwołaniem o wartości null (nic w Visual Basic).

siostra
źródło
5
Reflector mówi mi, że ObjectIDGenerator jest tablicą haszującą opierającą się na domyślnej implementacji GetHashCode (tj. Nie używa przeciążeń użytkownika).
Anton Tykhyy
Prawdopodobnie najlepsze rozwiązanie, gdy wymagane są unikalne identyfikatory do wydrukowania.
Roman Starkov
ObjectIDGenerator również nie jest zaimplementowany w telefonie.
Anthony Wieser
Nie rozumiem dokładnie, co robi ObjectIDGenerator, ale wydaje się, że działa, nawet jeśli używa RuntimeHelpers.GetHashCode. Przetestowałem oba i tylko RuntimeHelpers.GetHashCode nie działa w moim przypadku.
Daniel Bişar
+1 - Działa całkiem sprytnie (przynajmniej na pulpicie).
Hot Licks
37

RuntimeHelpers.GetHashCode()może pomóc ( MSDN ).

Anton Gogolev
źródło
2
To może dobrze pomóc, ale z kosztem - IIRC, używając obiektu podstawowego. GetHashCode () musi przydzielić blok synchronizacji, który nie jest darmowy. Niezły pomysł - +1 ode mnie.
Jon Skeet
Dzięki, nie znałem tej metody. Jednak nie generuje również unikalnego kodu skrótu (zachowuje się dokładnie tak samo, jak przykładowy kod w pytaniu). Przyda się jednak, jeśli użytkownik przesłoni kod skrótu, aby wywołać wersję domyślną.
Martin Konicek
1
Możesz użyć GCHandle, jeśli nie potrzebujesz ich zbyt wiele (patrz poniżej).
Anton Tykhyy
42
W książce na temat .NET autorstwa bardzo szanowanego autora stwierdzono, że RuntimeHelpers.GetHashCode () wygeneruje kod, który jest unikalny w domenie AppDomain, a firma Microsoft mogła nazwać metodę GetUniqueObjectID. To jest po prostu złe. Podczas testów stwierdziłem, że zwykle otrzymuję duplikat, zanim utworzę 10000 instancji obiektu (pole tekstowe WinForms) i nigdy nie przekroczyłem 30000. Kod oparty na rzekomej wyjątkowości powodował sporadyczne awarie w systemie produkcyjnym po utworzeniu nie więcej niż 1/10 tej liczby obiektów.
Jan Hettich
3
@supercat: Aha - właśnie znalazłem pewne dowody z 2003 r., które pochodziły z .NET 1.0 i 1.1. Wygląda na to, że planowali przejść na .NET 2: blogs.msdn.com/b/brada/archive/2003/09/30/50396.aspx
Jon Skeet
7

Możesz rozwinąć swoją własną rzecz w ciągu sekundy. Na przykład:

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

Możesz samodzielnie wybrać, co chcesz mieć jako unikalny identyfikator, na przykład System.Guid.NewGuid () lub po prostu liczbę całkowitą, aby uzyskać najszybszy dostęp.

majkinetor
źródło
2
Nie pomoże, jeśli potrzebujesz tego do Disposebłędów, ponieważ uniemożliwiłoby to jakąkolwiek utylizację.
Roman Starkov
1
To nie do końca działa jako słowniku rozchodów równości zamiast tożsamości zawaleniem obiektów, które zwracają te same wartości dla object.Equals
Anthony Wieser
1
To jednak utrzyma obiekt przy życiu.
Martin Lottering
1
@MartinLottering a co, jeśli używa ConditionalWeakTable <object, idType>?
Demetris Leptos
7

A co z tą metodą:

Ustaw pole w pierwszym obiekcie na nową wartość. Jeśli to samo pole w drugim obiekcie ma tę samą wartość, prawdopodobnie jest to ta sama instancja. W przeciwnym razie zakończ jak inaczej.

Teraz ustaw pole w pierwszym obiekcie na inną nową wartość. Jeśli to samo pole w drugim obiekcie zmieniło się na inną wartość, jest to zdecydowanie ta sama instancja.

Nie zapomnij przywrócić pierwotnej wartości pola w pierwszym obiekcie przy wyjściu.

Problemy?

Dawg
źródło
4

Możliwe jest utworzenie unikatowego identyfikatora obiektu w programie Visual Studio: w oknie obserwacyjnym kliknij prawym przyciskiem myszy zmienną obiektu i wybierz Utwórz identyfikator obiektu z menu kontekstowego.

Niestety jest to krok ręczny i nie sądzę, aby można było uzyskać dostęp do identyfikatora za pomocą kodu.

Thomas Bratt
źródło
Jakie wersje programu Visual Studio mają tę funkcję? Na przykład wersje Express?
Peter Mortensen,
3

Musiałbyś sam przypisać taki identyfikator, ręcznie - wewnątrz instancji lub zewnętrznie.

W przypadku rekordów związanych z bazą danych klucz podstawowy może być przydatny (ale nadal można uzyskać duplikaty). Alternatywnie, użyj Guidlub zachowaj własny licznik, alokując za pomocą Interlocked.Increment(i uczyń go na tyle dużym, aby nie przepełniał).

Marc Gravell
źródło
1

Informacje, które tu podaję, nie są nowe, dodałem je tylko dla kompletności.

Idea tego kodu jest dość prosta:

  • Obiekty wymagają unikalnego identyfikatora, którego domyślnie nie ma. Zamiast tego musimy polegać na następnej najlepszej rzeczy, czyli RuntimeHelpers.GetHashCodena uzyskaniu swego rodzaju unikalnego identyfikatora
  • Aby sprawdzić wyjątkowość, oznacza to, że musimy użyć object.ReferenceEquals
  • Jednak nadal chcielibyśmy mieć unikalny identyfikator, więc dodałem GUID, który z definicji jest unikalny.
  • Ponieważ nie lubię blokować wszystkiego, jeśli nie muszę, nie używam ConditionalWeakTable.

W połączeniu daje to następujący kod:

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

Aby z niego skorzystać, utwórz instancję UniqueIdMapperi użyj identyfikatorów GUID, które zwraca dla obiektów.


Uzupełnienie

Tak więc dzieje się tu trochę więcej; napiszę trochę o tym ConditionalWeakTable.

ConditionalWeakTablerobi kilka rzeczy. Najważniejsze jest to, że nie obchodzi go garbage collector, to znaczy: obiekty, do których odwołujesz się w tej tabeli, zostaną zebrane niezależnie. Jeśli szukasz obiektu, działa on w zasadzie tak samo, jak powyższy słownik.

Ciekawy nie? W końcu, gdy obiekt jest zbierany przez GC, sprawdza, czy istnieją odwołania do obiektu, a jeśli tak, zbiera je. Więc jeśli istnieje obiekt z ConditionalWeakTable, dlaczego w takim przypadku będzie zbierany obiekt , do którego się odwołuje?

ConditionalWeakTableużywa małej sztuczki, której również używają inne struktury .NET: zamiast przechowywać odniesienie do obiektu, w rzeczywistości przechowuje IntPtr. Ponieważ to nie jest prawdziwe odniesienie, obiekt można zebrać.

W tym momencie należy rozwiązać dwa problemy. Po pierwsze, obiekty można przenosić na stercie, więc czego użyjemy jako IntPtr? Po drugie, skąd wiemy, że obiekty mają aktywne odniesienie?

  • Obiekt można przypiąć na stercie, a jego rzeczywisty wskaźnik można przechowywać. Kiedy GC uderza w obiekt w celu usunięcia, odpina go i zbiera. Oznaczałoby to jednak, że otrzymujemy przypięty zasób, co nie jest dobrym pomysłem, jeśli masz dużo obiektów (ze względu na problemy z fragmentacją pamięci). Prawdopodobnie tak nie działa.
  • Kiedy GC przesuwa obiekt, wywołuje z powrotem, który może następnie zaktualizować odwołania. Może tak jest zaimplementowany, sądząc po połączeniach zewnętrznych DependentHandle- ale uważam, że jest nieco bardziej wyrafinowany.
  • Nie wskaźnik do samego obiektu, ale wskaźnik na liście wszystkich obiektów z GC jest przechowywany. IntPtr jest indeksem lub wskaźnikiem na tej liście. Lista zmienia się tylko wtedy, gdy obiekt zmienia generacje, w którym to momencie proste wywołanie zwrotne może zaktualizować wskaźniki. Jeśli pamiętasz, jak działa Mark & ​​Sweep, ma to większy sens. Nie ma przypinania, a usuwanie jest takie, jak wcześniej. Myślę, że tak to działa DependentHandle.

To ostatnie rozwiązanie wymaga, aby środowisko wykonawcze nie użyło ponownie zasobników listy, dopóki nie zostaną one jawnie zwolnione, a także wymaga, aby wszystkie obiekty zostały pobrane przez wywołanie środowiska wykonawczego.

Jeśli założymy, że korzystają z tego rozwiązania, możemy również rozwiązać drugi problem. Algorytm Mark & ​​Sweep śledzi, które obiekty zostały zebrane; jak tylko zostanie zebrany, wiemy w tym miejscu. Gdy obiekt sprawdzi, czy obiekt tam jest, wywołuje „Free”, co usuwa wskaźnik i wpis na liście. Obiekt naprawdę zniknął.

W tym miejscu należy zauważyć, że rzeczy idą strasznie źle, jeśli ConditionalWeakTablesą aktualizowane w wielu wątkach i nie są bezpieczne dla wątków. Rezultatem byłby wyciek pamięci. Dlatego wszystkie połączenia ConditionalWeakTablewykonują prostą „blokadę”, która zapewnia, że ​​tak się nie stanie.

Inną rzeczą, na którą należy zwrócić uwagę, jest to, że czyszczenie wpisów musi odbywać się od czasu do czasu. Chociaż rzeczywiste obiekty zostaną wyczyszczone przez GC, wpisy nie. Dlatego ConditionalWeakTabletylko rośnie. Gdy osiągnie pewien limit (określony przez szansę na kolizję w hashu), wyzwala a Resize, który sprawdza, czy obiekty muszą zostać wyczyszczone - jeśli tak, freejest wywoływane w procesie GC, usuwając IntPtruchwyt.

Uważam, że jest to również powód, dla którego DependentHandlenie jest on ujawniany bezpośrednio - nie chcesz zepsuć rzeczy i w rezultacie uzyskać wyciek pamięci. Następną najlepszą rzeczą do tego jest a WeakReference(który również przechowuje IntPtrzamiast obiektu) - ale niestety nie obejmuje aspektu „zależności”.

Pozostaje ci bawić się mechaniką, abyś mógł zobaczyć zależność w akcji. Pamiętaj, aby uruchomić go wiele razy i obserwować wyniki:

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
    }
atlaste
źródło
1
A ConditionalWeakTablemoże być lepsze, ponieważ utrzymywałoby reprezentacje obiektów tylko wtedy, gdy istnieją do nich odniesienia. Sugerowałbym również, że Int64może być lepszy niż identyfikator GUID, ponieważ pozwoliłby na nadanie obiektom trwałej rangi . Takie rzeczy mogą być przydatne w scenariuszach blokowania (np. Można uniknąć zakleszczenia, jeśli jeden cały kod, który będzie musiał uzyskać wiele zamków, robi to w określonej kolejności, ale aby to zadziałało, musi być określona kolejność).
supercat
@supercat Jasne co do longs; to zależy od twojego scenariusza - np. systemy rozproszone czasami bardziej przydatna jest praca z GUIDs. A co do ConditionalWeakTable: masz rację; DependentHandlesprawdza żywotność (UWAGA: tylko wtedy, gdy rzecz zmienia rozmiar!), co może być tutaj przydatne. Mimo to, jeśli potrzebujesz wydajności, blokowanie może stać się tam problemem, więc w takim przypadku może być interesujące skorzystanie z tego ... szczerze mówiąc osobiście nie lubię implementacji ConditionalWeakTable, co prawdopodobnie prowadzi do mojej stronniczości korzystania z prostego Dictionary- równego chociaż masz rację.
atlaste
Od dawna ciekawiło mnie, jak to ConditionalWeakTablewłaściwie działa. Fakt, że pozwala tylko na dodawanie elementów, sprawia, że ​​myślę, że został zaprojektowany, aby zminimalizować narzut związany ze współbieżnością, ale nie mam pojęcia, jak to działa wewnętrznie. Ciekawe, że nie ma prostego DependentHandleopakowania, które nie używa stołu, ponieważ z pewnością są chwile, kiedy ważne jest, aby jeden obiekt był żywy przez całe życie innego, ale ten drugi obiekt nie ma miejsca na odniesienie do pierwszego.
supercat
@supercat Opublikuję dodatek na temat tego, jak myślę, że to działa.
atlaste
Te ConditionalWeakTablewpisy nie pozwala, które zostały zapisane w tabeli, aby być modyfikowane. W związku z tym myślę, że można go bezpiecznie zaimplementować za pomocą barier pamięci, ale nie blokad. Jedyną problematyczną sytuacją byłoby, gdyby dwa wątki próbowały dodać ten sam klucz jednocześnie; Problem ten można rozwiązać, wykonując za pomocą metody „add” barierę pamięci po dodaniu elementu, a następnie skanując, aby upewnić się, że klucz ma dokładnie jeden element. Jeśli wiele elementów ma ten sam klucz, jeden z nich będzie identyfikowany jako „pierwszy”, więc możliwe będzie wyeliminowanie pozostałych.
supercat
0

Jeśli piszesz moduł we własnym kodzie do określonego zastosowania, metoda majkinetora MOŻE zadziałać. Ale są pewne problemy.

Po pierwsze , oficjalny dokument NIE gwarantuje, że GetHashCode()zwraca unikalny identyfikator (patrz Metoda Object.GetHashCode () ):

Nie należy zakładać, że równe kody skrótów implikują równość obiektów.

Po drugie , załóżmy, że masz bardzo małą liczbę obiektów, więc GetHashCode()w większości przypadków będzie działać, ta metoda może zostać zastąpiona przez niektóre typy.
Na przykład, używasz pewnej klasy C i nadpisuje GetHashCode()ona zawsze zwracanie 0. Wtedy każdy obiekt C otrzyma ten sam kod skrótu. Niestety Dictionary, HashTablei kilka innych asocjacyjne pojemniki będą wykorzystywać tę metodę:

Kod skrótu to wartość liczbowa używana do wstawiania i identyfikowania obiektu w kolekcji opartej na skrótach, takiej jak klasa Dictionary <TKey, TValue>, klasa Hashtable lub typ pochodzący z klasy DictionaryBase. Metoda GetHashCode zapewnia ten kod skrótu dla algorytmów, które wymagają szybkiego sprawdzenia równości obiektów.

Tak więc to podejście ma wielkie ograniczenia.

A co więcej , jeśli chcesz zbudować bibliotekę ogólnego przeznaczenia? Nie tylko nie jesteś w stanie modyfikować kodu źródłowego użytych klas, ale także ich zachowanie jest nieprzewidywalne.

Doceniam, że Jon i Simon opublikowali swoje odpowiedzi, a poniżej zamieszczę przykładowy kod i sugestię dotyczącą wydajności.

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

W moim teście ObjectIDGeneratorzgłosi wyjątek, aby narzekać, że podczas tworzenia 10000000 obiektów jest zbyt wiele obiektów (10x niż w powyższym kodzie) w forpętli.

Wynik testu porównawczego jest taki, że ConditionalWeakTableimplementacja jest 1,8x szybsza niż ObjectIDGeneratorimplementacja.

Panie Ree
źródło