Identificador de objeto exclusivo do .NET

118

Existe uma maneira de obter um identificador exclusivo de uma instância?

GetHashCode()é o mesmo para as duas referências que apontam para a mesma instância. No entanto, duas instâncias diferentes podem (facilmente) obter o mesmo código hash:

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

Estou escrevendo um addin de depuração e preciso obter algum tipo de ID para uma referência que seja única durante a execução do programa.

Já consegui o ADDRESS interno da instância, que é único até que o garbage collector (GC) compacta o heap (= move os objetos = muda os endereços).

Pergunta sobre Stack Overflow A implementação padrão para Object.GetHashCode () pode estar relacionada.

Os objetos não estão sob meu controle, pois estou acessando objetos em um programa que está sendo depurado usando a API do depurador. Se eu estivesse no controle dos objetos, adicionar meus próprios identificadores exclusivos seria trivial.

Eu queria o ID exclusivo para construir um ID hashtable -> objeto, para poder pesquisar objetos já vistos. Por enquanto eu resolvi assim:

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
fonte

Respostas:

42

A referência é o identificador exclusivo do objeto. Não conheço nenhuma maneira de converter isso em algo como uma string etc. O valor da referência mudará durante a compactação (como você viu), mas todos os valores A anteriores serão alterados para o valor B, até agora no que diz respeito ao código seguro, ainda é um ID único.

Se os objetos envolvidos estiverem sob seu controle, você pode criar um mapeamento usando referências fracas (para evitar a coleta de lixo) de uma referência a um ID de sua escolha (GUID, inteiro, qualquer que seja). No entanto, isso acrescentaria uma certa sobrecarga e complexidade.

Jon Skeet
fonte
1
Eu acho que para pesquisas, você teria que iterar sobre todas as referências que rastreia: WeakReference para o mesmo objeto não são iguais entre si, então você realmente não pode fazer muito mais.
Roman Starkov
1
Pode ser útil ter cada objeto atribuído a um ID exclusivo de 64 bits, especialmente se esses IDs forem emitidos sequencialmente. Não tenho certeza se a utilidade justificaria o custo, mas tal coisa poderia ser útil se alguém comparar dois objetos imutáveis ​​distintos e os achar iguais; se um, quando possível, sobrescrever a referência ao mais novo com uma referência ao mais antigo, pode-se evitar ter muitas referências redundantes para objetos idênticos, mas distintos.
supercat
1
“Identificador.” Não acho que essa palavra signifique o que você pensa que significa.
Slipp D. Thompson
5
@ SlippD.Thompson: Não, ainda é uma relação de 1 para 1. Existe apenas um único valor de referência que se refere a um determinado objeto. Esse valor pode aparecer muitas vezes na memória (por exemplo, como o valor de várias variáveis), mas ainda é um valor único. É como um endereço residencial: posso anotar meu endereço residencial em vários pedaços de papel, mas esse ainda é o identificador de minha casa. Quaisquer dois valores de referência não idênticos devem se referir a objetos diferentes - pelo menos em C #.
Jon Skeet
1
@supercat: Acho que podemos diferir em nosso entendimento de "identidades sendo encapsuladas" - mas acho que provavelmente também não estamos ajudando ninguém a ir além do que já fizemos :) Apenas um dos tópicos que devemos discutir longamente se sempre nos encontramos pessoalmente ...
Jon Skeet
72

.NET 4 e posterior apenas

Boas notícias, pessoal!

A ferramenta perfeita para esse trabalho é construída em .NET 4 e é chamada ConditionalWeakTable<TKey, TValue>. Esta aula:

  • pode ser usado para associar dados arbitrários a instâncias de objetos gerenciados, como um dicionário (embora não seja um dicionário)
  • não depende de endereços de memória, portanto, é imune ao GC compactando o heap
  • não mantém os objetos vivos apenas porque foram inseridos como chaves na mesa, portanto, pode ser usado sem fazer com que todos os objetos em seu processo vivam para sempre
  • usa igualdade de referência para determinar a identidade do objeto; Além disso, os autores da classe não podem modificar este comportamento, então ele pode ser usado consistentemente em objetos de qualquer tipo
  • pode ser preenchido na hora, portanto, não requer que você injete código dentro de construtores de objeto
Jon
fonte
5
Apenas para completar: ConditionalWeakTableconfia em RuntimeHelpers.GetHashCodee object.ReferenceEqualspara fazer seu funcionamento interno. O comportamento é o mesmo que construir um IEqualityComparer<T>que use esses dois métodos. Se você precisa de desempenho, eu realmente sugiro fazer isso, já que ConditionalWeakTabletem um bloqueio em torno de todas as suas operações para torná-lo seguro para thread.
atlaste
1
@StefandeBruijn: A ConditionalWeakTablecontém uma referência a cada um Valueque é tão forte quanto a referência mantida em outro lugar para o correspondente Key. Um objeto para o qual a ConditionalWeakTablecontém a única referência existente em qualquer lugar do universo deixará de existir automaticamente quando a chave o fizer.
supercat
41

Check-out do ObjectIDGenerator classe? Isso faz o que você está tentando fazer e o que Marc Gravell descreve.

O ObjectIDGenerator rastreia os objetos identificados anteriormente. Quando você pede o ID de um objeto, o ObjectIDGenerator sabe se deve retornar o ID existente ou gerar e lembrar um novo ID.

Os IDs são exclusivos durante a vida da instância ObjectIDGenerator. Geralmente, a vida de um ObjectIDGenerator dura tanto quanto o Formatador que o criou. Os IDs de objeto têm significado apenas em um determinado fluxo serializado e são usados ​​para rastrear quais objetos têm referências a outros dentro do gráfico de objeto serializado.

Usando uma tabela hash, o ObjectIDGenerator retém qual ID é atribuído a qual objeto. As referências de objeto, que identificam exclusivamente cada objeto, são endereços no heap de coleta de lixo do tempo de execução. Os valores de referência do objeto podem mudar durante a serialização, mas a tabela é atualizada automaticamente para que as informações estejam corretas.

Os IDs de objeto são números de 64 bits. A alocação começa em um, portanto, zero nunca é um ID de objeto válido. Um formatador pode escolher um valor zero para representar uma referência de objeto cujo valor é uma referência nula (Nothing em Visual Basic).

irmã
fonte
5
O Reflector me diz que ObjectIDGenerator é um hashtable baseado na implementação GetHashCode padrão (ou seja, não usa sobrecargas de usuário).
Anton Tykhyy,
Provavelmente, a melhor solução quando IDs exclusivos para impressão são necessários.
Roman Starkov
ObjectIDGenerator também não está implementado no telefone.
Anthony Wieser,
Não entendo exatamente o que ObjectIDGenerator está fazendo, mas parece funcionar, mesmo quando está usando RuntimeHelpers.GetHashCode. Testei ambos e apenas RuntimeHelpers.GetHashCode falha no meu caso.
Daniel Bişar
+1 - Funciona muito bem (no desktop, pelo menos).
Hot Licks
37

RuntimeHelpers.GetHashCode()pode ajudar ( MSDN ).

Anton Gogolev
fonte
2
Isso pode muito bem ajudar, mas com um custo - IIRC, usando o objeto base.GetHashCode () precisa alocar um bloco de sincronização, que não é gratuito. Boa ideia, porém - um de mim.
Jon Skeet,
Obrigado, eu não conhecia esse método. No entanto, ele também não produz código hash exclusivo (se comporta exatamente da mesma forma que o código de amostra na pergunta). No entanto, será útil se o usuário substituir o código hash, para chamar a versão padrão.
Martin Konicek
1
Você pode usar o GCHandle se não precisar de muitos deles (veja abaixo).
Anton Tykhyy
42
Um livro sobre .NET de um autor altamente respeitado afirma que RuntimeHelpers.GetHashCode () produzirá um código exclusivo em um AppDomain e que a Microsoft poderia ter chamado o método GetUniqueObjectID. Isso está simplesmente errado. Nos testes, descobri que normalmente obteria uma duplicata no momento em que tivesse criado 10.000 instâncias de um objeto (um WinForms TextBox) e nunca poderia passar de 30.000. O código que dependia da suposta exclusividade estava causando travamentos intermitentes em um sistema de produção após a criação de no máximo 1/10 dessa quantidade de objetos.
Jan Hettich
3
@supercat: Aha - acabei de encontrar algumas evidências, de 2003, que eram do .NET 1.0 e 1.1. Parece que eles estavam planejando mudar para o .NET 2: blogs.msdn.com/b/brada/archive/2003/09/30/50396.aspx
Jon Skeet
7

Você pode desenvolver suas próprias coisas em um segundo. Por exemplo:

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

Você pode escolher o que deseja ter como ID exclusivo por conta própria, por exemplo, System.Guid.NewGuid () ou simplesmente inteiro para acesso mais rápido.

majkinetor
fonte
2
Não adianta se você precisa disso são Disposebugs, pois evitaria qualquer tipo de descarte.
Roman Starkov
1
Isso não funciona muito bem, pois o dicionário usa igualdade em vez de identidade, recolhendo objetos que retornam os mesmos valores para o objeto
Anthony Wieser
1
Isso irá manter o objeto vivo.
Martin Lottering
1
@MartinLottering e se ele usar ConditionalWeakTable <object, idType>?
Demetris Leptos
7

Que tal este método:

Defina um campo no primeiro objeto com um novo valor. Se o mesmo campo no segundo objeto tiver o mesmo valor, provavelmente é a mesma instância. Caso contrário, saia como diferente.

Agora defina o campo no primeiro objeto com um novo valor diferente. Se o mesmo campo no segundo objeto mudou para um valor diferente, é definitivamente a mesma instância.

Não se esqueça de definir o campo no primeiro objeto de volta ao seu valor original ao sair.

Problemas?

Dawg
fonte
4

É possível criar um identificador de objeto exclusivo no Visual Studio: Na janela de observação, clique com o botão direito do mouse na variável do objeto e escolha Criar ID do objeto no menu de contexto.

Infelizmente, esta é uma etapa manual e não acredito que o identificador possa ser acessado via código.

Thomas Bratt
fonte
Quais versões do Visual Studio possuem esse recurso? Por exemplo, as versões Express?
Peter Mortensen,
3

Você mesmo teria que atribuir tal identificador, manualmente - seja dentro da instância ou externamente.

Para registros relacionados a um banco de dados, a chave primária pode ser útil (mas você ainda pode obter duplicatas). Alternativamente, use a Guidou mantenha seu próprio contador, alocando usando Interlocked.Increment(e torne-o grande o suficiente para que não transborde).

Marc Gravell
fonte
1

As informações que dou aqui não são novas, apenas adicionei para completar.

A ideia deste código é bastante simples:

  • Os objetos precisam de um ID exclusivo, que não existe por padrão. Em vez disso, temos que confiar na próxima melhor coisa, que é RuntimeHelpers.GetHashCodenos fornecer uma espécie de ID exclusivo
  • Para verificar a exclusividade, isso implica que precisamos usar object.ReferenceEquals
  • No entanto, ainda gostaríamos de ter um ID exclusivo, então adicionei um GUID, que por definição é exclusivo.
  • Porque não gosto de trancar tudo se não for preciso, não uso ConditionalWeakTable.

Combinado, isso fornecerá o seguinte código:

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

Para usá-lo, crie uma instância de UniqueIdMappere use os GUIDs que retorna para os objetos.


Termo aditivo

Então, há um pouco mais acontecendo aqui; deixe-me escrever um pouco sobre ConditionalWeakTable.

ConditionalWeakTablefaz algumas coisas. O mais importante é que não se preocupe com o coletor de lixo, ou seja: os objetos que você referenciar nesta tabela serão coletados de qualquer maneira. Se você pesquisar um objeto, ele basicamente funcionará da mesma forma que o dicionário acima.

Curioso, não? Afinal, quando um objeto está sendo coletado pelo CG, ele verifica se há referências ao objeto e, se houver, as coleta. Portanto, se houver um objeto do ConditionalWeakTable, por que o objeto referenciado será coletado?

ConditionalWeakTableusa um pequeno truque, que algumas outras estruturas .NET também usam: em vez de armazenar uma referência ao objeto, ele na verdade armazena um IntPtr. Como essa não é uma referência real, o objeto pode ser coletado.

Portanto, neste ponto, há 2 problemas a serem resolvidos. Primeiro, os objetos podem ser movidos no heap, então o que usaremos como IntPtr? E em segundo lugar, como sabemos que os objetos têm uma referência ativa?

  • O objeto pode ser fixado no heap e seu ponteiro real pode ser armazenado. Quando o GC atinge o objeto para remoção, ele o desenrosca e o coleta. No entanto, isso significaria que obteríamos um recurso fixado, o que não é uma boa ideia se você tiver muitos objetos (devido a problemas de fragmentação de memória). Provavelmente não é assim que funciona.
  • Quando o GC move um objeto, ele chama de volta, que pode então atualizar as referências. Pode ser assim que está implementado a julgar pelas chamadas externas DependentHandle- mas acredito que seja um pouco mais sofisticado.
  • Não o ponteiro para o próprio objeto, mas um ponteiro na lista de todos os objetos do GC é armazenado. O IntPtr é um índice ou um ponteiro nesta lista. A lista só muda quando um objeto muda de geração, ponto em que um simples retorno de chamada pode atualizar os ponteiros. Se você se lembra de como funciona o Mark & ​​Sweep, isso faz mais sentido. Não há fixação e a remoção é como antes. Eu acredito que é assim que funciona no DependentHandle.

Esta última solução exige que o tempo de execução não reutilize os depósitos de lista até que eles sejam explicitamente liberados e também exige que todos os objetos sejam recuperados por uma chamada para o tempo de execução.

Se presumirmos que eles usam essa solução, também podemos resolver o segundo problema. O algoritmo Mark & ​​Sweep mantém registro de quais objetos foram coletados; assim que for coletado, sabemos neste ponto. Depois que o objeto verifica se o objeto está lá, ele chama 'Livre', que remove o ponteiro e a entrada da lista. O objeto realmente se foi.

Uma coisa importante a se observar neste ponto é que as coisas dão terrivelmente errado se ConditionalWeakTablefor atualizado em vários threads e não for seguro para threads. O resultado seria um vazamento de memória. É por isso que todas as chamadas ConditionalWeakTablefazem um simples 'bloqueio' que garante que isso não aconteça.

Outra coisa a observar é que a limpeza das entradas deve acontecer de vez em quando. Embora os objetos reais sejam limpos pelo GC, as entradas não são. É por isso que ConditionalWeakTableapenas cresce em tamanho. Uma vez que atinge um certo limite (determinado pela chance de colisão no hash), ele aciona um Resize, que verifica se os objetos precisam ser limpos - se o fizerem, freeé chamado no processo de GC, removendo o IntPtridentificador.

Eu acredito que é também por isso que DependentHandlenão é exposto diretamente - você não quer bagunçar as coisas e ter um vazamento de memória como resultado. A próxima melhor coisa para isso é um WeakReference(que também armazena um em IntPtrvez de um objeto) - mas infelizmente não inclui o aspecto de 'dependência'.

O que resta é você brincar com a mecânica, para poder ver a dependência em ação. Certifique-se de iniciá-lo várias vezes e observar os resultados:

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
fonte
1
A ConditionalWeakTablepoderia ser melhor, já que só persistiria as representações de objetos enquanto existissem referências a eles. Além disso, sugiro que um Int64pode ser melhor do que um GUID, pois permite que os objetos recebam uma classificação persistente . Essas coisas podem ser úteis em cenários de bloqueio (por exemplo, pode-se evitar o deadlock se todo o código que precisará adquirir vários bloqueios o fizer em alguma ordem definida, mas para que isso funcione deve haver uma ordem definida).
supercat
@supercat Claro sobre o longs; depende do seu cenário - em f.ex. sistemas distribuídos às vezes é mais útil trabalhar com GUIDs. Quanto a ConditionalWeakTable: você está certo; DependentHandleverifica a vitalidade (NOTA: somente quando a coisa é redimensionada!), o que pode ser útil aqui. Ainda assim, se você precisar de desempenho, o bloqueio pode se tornar um problema, então, nesse caso, pode ser interessante usar isso ... para ser honesto, eu pessoalmente não gosto da implementação de ConditionalWeakTable, o que provavelmente leva ao meu preconceito de usar um simples Dictionary- uniforme embora você esteja correto.
atlaste
Há muito tempo estou curioso sobre como ConditionalWeakTablerealmente funciona. O fato de que ele só permite a adição de itens me faz pensar que ele foi projetado para minimizar a sobrecarga relacionada à concorrência, mas não tenho ideia de como funciona internamente. Acho curioso que não exista um DependentHandleinvólucro simples que não use uma tabela, já que definitivamente há momentos em que é importante garantir que um objeto seja mantido vivo pelo tempo de vida de outro, mas o último objeto não tem espaço para uma referência para o primeiro.
supercat
@supercat Vou postar um adendo sobre como acho que funciona.
atlaste
O ConditionalWeakTablenão permite que as entradas que foram armazenadas na tabela sejam modificadas. Como tal, eu acho que poderia ser implementado com segurança usando barreiras de memória, mas não bloqueios. A única situação problemática seria se dois threads tentassem adicionar a mesma chave simultaneamente; isso pode ser resolvido fazendo com que o método "adicionar" execute uma barreira de memória depois que um item é adicionado e, em seguida, escaneie para garantir que exatamente um item tenha essa chave. Se vários itens tiverem a mesma chave, um deles será identificável como "primeiro", portanto, será possível eliminar os outros.
supercat
0

Se você está escrevendo um módulo em seu próprio código para um uso específico, o método do majkinetor PODE ter funcionado. Mas existem alguns problemas.

Em primeiro lugar , o documento oficial NÃO garante que o GetHashCode()retorne um identificador exclusivo (consulte Object.GetHashCode Method () ):

Você não deve presumir que códigos hash iguais implicam igualdade de objeto.

Em segundo lugar , suponha que você tenha uma quantidade muito pequena de objetos para que GetHashCode()funcione na maioria dos casos, este método pode ser substituído por alguns tipos.
Por exemplo, você está usando alguma classe C e ela sobrescreve GetHashCode()para sempre retornar 0. Então, cada objeto de C obterá o mesmo código hash. Infelizmente, Dictionary, HashTablee alguns outros recipientes associativas fará usar este método:

Um código hash é um valor numérico usado para inserir e identificar um objeto em uma coleção baseada em hash, como a classe Dictionary <TKey, TValue>, a classe Hashtable ou um tipo derivado da classe DictionaryBase. O método GetHashCode fornece esse código hash para algoritmos que precisam de verificações rápidas de igualdade de objeto.

Portanto, essa abordagem tem grandes limitações.

E mais ainda , e se você quiser construir uma biblioteca de uso geral? Não apenas você não pode modificar o código-fonte das classes utilizadas, mas seu comportamento também é imprevisível.

Agradeço que Jon e Simon tenham postado suas respostas e postarei um exemplo de código e uma sugestão de desempenho abaixo.

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

Em meu teste, o ObjectIDGeneratorlançará uma exceção para reclamar que há muitos objetos ao criar 10.000.000 objetos (10x do que no código acima) no forloop.

Além disso, o resultado do benchmark é que a ConditionalWeakTableimplementação é 1,8x mais rápida do que a ObjectIDGeneratorimplementação.

Sr. Ree
fonte