c# - Identificador de objeto único.NET
gethashcode c# override (11)
.NET 4 y posterior solamente
¡Buenas noticias para todos!
La herramienta perfecta para este trabajo está construida en .NET 4 y se llama ConditionalWeakTable<TKey, TValue>
. Esta clase:
- se puede usar para asociar datos arbitrarios con instancias de objetos administrados de manera muy parecida a un diccionario (aunque no es un diccionario)
- no depende de las direcciones de memoria, por lo que es inmune al GC compactando el montón
- no mantiene vivos los objetos solo porque se han ingresado como claves en la tabla, por lo que puede usarse sin hacer que todos los objetos de su proceso vivan para siempre
- usa la igualdad de referencia para determinar la identidad del objeto; moveover, los autores de la clase no pueden modificar este comportamiento por lo que se puede utilizar de forma coherente en objetos de cualquier tipo
- puede ser poblado sobre la marcha, por lo que no requiere que se inyecte código dentro de los constructores de objetos
¿Hay alguna forma de obtener un identificador único de una instancia?
GetHashCode()
es el mismo para las dos referencias que apuntan a la misma instancia. Sin embargo, dos instancias diferentes pueden (bastante fácilmente) obtener el mismo 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);
}
Estoy escribiendo un complemento de depuración, y necesito obtener algún tipo de ID para una referencia que sea única durante la ejecución del programa.
Ya logré obtener una DIRECCIÓN interna de la instancia, que es única hasta que el recolector de basura (GC) compacta el montón (= mueve los objetos = cambia las direcciones).
Pregunta de desbordamiento de pila La implementación predeterminada para Object.GetHashCode () podría estar relacionada.
Los objetos no están bajo mi control ya que estoy accediendo a objetos en un programa que se depura utilizando la API del depurador. Si tuviera el control de los objetos, agregar mis propios identificadores únicos sería trivial.
Quería la ID única para construir un identificador hashtable -> object, para poder buscar objetos ya vistos. Por ahora lo resolví así:
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
}
¿Qué tal este método?
Establezca un campo en el primer objeto a un nuevo valor. Si el mismo campo en el segundo objeto tiene el mismo valor, es probable que sea la misma instancia. De lo contrario, salga como diferente.
Ahora configure el campo en el primer objeto con un nuevo valor diferente. Si el mismo campo en el segundo objeto ha cambiado a un valor diferente, definitivamente es la misma instancia.
No te olvides de configurar el campo en el primer objeto de nuevo a su valor original al salir.
¿Problemas?
¿Revisó la clase ObjectIDGenerator ? Esto hace lo que estás intentando hacer, y lo que Marc Gravell describe.
ObjectIDGenerator realiza un seguimiento de los objetos identificados previamente. Cuando solicita el ID de un objeto, ObjectIDGenerator sabe si debe devolver el ID existente, o generar y recordar un nuevo ID.
Los ID son únicos durante la vida de la instancia de ObjectIDGenerator. Generalmente, una vida de ObjectIDGenerator dura tanto como el formateador que la creó. Los ID de objeto tienen significado solo dentro de un flujo serializado dado, y se usan para rastrear qué objetos tienen referencias a otros dentro del gráfico de objeto serializado.
Usando una tabla hash, ObjectIDGenerator retiene qué ID se asigna a qué objeto. Las referencias a objetos, que identifican de forma única a cada objeto, son direcciones en el montón recolectado de basura en tiempo de ejecución. Los valores de referencia de objeto pueden cambiar durante la serialización, pero la tabla se actualiza automáticamente para que la información sea correcta.
Los identificadores de objeto son números de 64 bits. La asignación comienza desde uno, por lo que cero nunca es un ID de objeto válido. Un formateador puede elegir un valor cero para representar una referencia de objeto cuyo valor es una referencia nula (Nothing en Visual Basic).
Es posible crear un identificador de objeto único en Visual Studio: en la ventana de observación, haga clic con el botón derecho en la variable de objeto y elija Crear ID de objeto en el menú contextual.
Desafortunadamente, este es un paso manual, y no creo que se pueda acceder al identificador a través del código.
La información que doy aquí no es nueva, solo agregué esto para que esté completa.
La idea de este código es bastante simple:
- Los objetos necesitan una identificación única, que no está allí por defecto. En su lugar, tenemos que confiar en la siguiente mejor
RuntimeHelpers.GetHashCode
, que esRuntimeHelpers.GetHashCode
para obtener un tipo de ID único - Para verificar la singularidad, esto implica que necesitamos usar
object.ReferenceEquals
- Sin embargo, aún nos gustaría tener una ID única, así que agregué un
GUID
, que es por definición único. - Como no me gusta bloquear todo si no es necesario, no uso
ConditionalWeakTable
.
Combinado, que le dará el siguiente 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 usarlo, cree una instancia de UniqueIdMapper
y use los GUID que devuelve para los objetos.
Apéndice
Entonces, hay un poco más pasando aquí; déjame escribir un poco sobre ConditionalWeakTable
.
ConditionalWeakTable
hace un par de cosas. Lo más importante es que no le importa el recolector de basura, es decir: los objetos a los que hace referencia en esta tabla se recopilarán independientemente. Si busca un objeto, básicamente funciona igual que el diccionario anterior.
Curioso no? Después de todo, cuando un objeto está siendo recopilado por el GC, verifica si hay referencias al objeto, y si los hay, los recopila. Entonces, si hay un objeto de ConditionalWeakTable
, ¿por qué se recopilará el objeto referenciado?
ConditionalWeakTable
utiliza un pequeño truco, que algunas otras estructuras .NET también usan: en lugar de almacenar una referencia al objeto, en realidad almacena un IntPtr. Como esa no es una referencia real, el objeto se puede recopilar.
Entonces, en este punto hay 2 problemas que abordar. Primero, los objetos se pueden mover en el montón, entonces, ¿qué usaremos como IntPtr? Y segundo, ¿cómo sabemos que los objetos tienen una referencia activa?
- El objeto se puede anclar en el montón y su puntero real se puede almacenar. Cuando el GC golpea el objeto para su eliminación, lo desenchufa y lo recoge. Sin embargo, eso significaría que obtenemos un recurso fijo, lo que no es una buena idea si tiene muchos objetos (debido a problemas de fragmentación de la memoria). Probablemente no sea así como funciona.
- Cuando el GC mueve un objeto, devuelve la llamada, que luego puede actualizar las referencias. Esta podría ser la forma en que se implementa a juzgar por las llamadas externas en
DependentHandle
, pero creo que es un poco más sofisticado. - No se almacena el puntero al objeto en sí, sino un puntero en la lista de todos los objetos del GC. El IntPtr es un índice o un puntero en esta lista. La lista solo cambia cuando un objeto cambia generaciones, momento en el que una devolución de llamada simple puede actualizar los punteros. Si recuerda cómo funciona Mark & Sweep, tiene más sentido. No hay fijación, y la eliminación es como era antes. Creo que así es como funciona en
DependentHandle
.
Esta última solución requiere que el tiempo de ejecución no vuelva a utilizar los depósitos de la lista hasta que se liberen explícitamente, y también requiere que todos los objetos se recuperen mediante una llamada al tiempo de ejecución.
Si suponemos que usan esta solución, también podemos abordar el segundo problema. El algoritmo Mark & Sweep realiza un seguimiento de los objetos que se han recopilado; tan pronto como se haya recopilado, lo sabremos en este punto. Una vez que el objeto comprueba si el objeto está allí, llama ''Gratis'', lo que elimina el puntero y la entrada de la lista. El objeto realmente se ha ido.
Una cosa importante a tener en cuenta en este punto es que las cosas van terriblemente mal si ConditionalWeakTable
se actualiza en varios subprocesos y si no es seguro para subprocesos. El resultado sería una pérdida de memoria. Esta es la razón por la cual todas las llamadas en ConditionalWeakTable
hacen un simple ''bloqueo'' que asegura que esto no ocurra.
Otra cosa a tener en cuenta es que limpiar las entradas tiene que suceder de vez en cuando. Mientras que los objetos reales serán limpiados por el GC, las entradas no lo son. Esta es la razón por la cual ConditionalWeakTable
solo crece en tamaño. Una vez que alcanza cierto límite (determinado por la probabilidad de colisión en el hash), desencadena un Resize
, que verifica si los objetos deben limpiarse; si lo hacen, se llama a free
en el proceso GC, eliminando el identificador IntPtr
.
Creo que este es también el motivo por el que DependentHandle
no se expone directamente: no se quiere meter en problemas y, como resultado, se produce una pérdida de memoria. La siguiente mejor WeakReference
es una WeakReference
(que también almacena un IntPtr
lugar de un objeto), pero desafortunadamente no incluye el aspecto de "dependencia".
Lo que queda es que juegues con los mecánicos, para que puedas ver la dependencia en acción. Asegúrese de iniciarlo varias veces y ver los 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
}
La referencia es el identificador único para el objeto. No conozco ninguna forma de convertir esto en algo parecido a una cuerda, etc. El valor de la referencia cambiará durante la compactación (como has visto), pero cada valor anterior A se cambiará a valor B, por lo que hasta ahora como código seguro, sigue siendo una identificación única.
Si los objetos involucrados están bajo su control, puede crear una asignación utilizando referencias débiles (para evitar la prevención de la recolección de basura) de una referencia a una ID de su elección (GUID, entero, lo que sea). Eso agregaría una cierta cantidad de sobrecarga y complejidad, sin embargo.
Puedes desarrollar lo tuyo en un segundo. Por ejemplo:
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++;
}
}
Puede elegir lo que le gustaría tener como ID único por su cuenta, por ejemplo, System.Guid.NewGuid () o simplemente entero para un acceso más rápido.
Sé que esto ha sido respondido, pero al menos es útil tener en cuenta que puede usar:
http://msdn.microsoft.com/en-us/library/system.object.referenceequals.aspx
¿Qué no le dará un "ID único" directamente, pero combinado con WeakReferences (y un hashset?) Podría darle una manera bastante fácil de rastrear varias instancias.
Si está escribiendo un módulo en su propio código para un uso específico, el método de majkinetor PODRÍA haber funcionado. Pero hay algunos problemas.
Primero , el documento oficial NO garantiza que GetHashCode()
devuelva un identificador único (vea el método Object.GetHashCode () ):
No debe suponer que los códigos hash iguales implican igualdad de objetos.
Segundo , suponga que tiene una cantidad muy pequeña de objetos para que GetHashCode()
funcione en la mayoría de los casos, este método puede ser anulado por algunos tipos.
Por ejemplo, está utilizando alguna clase C y anula GetHashCode()
para devolver siempre 0. Entonces, cada objeto de C obtendrá el mismo código hash. Desafortunadamente, Dictionary
, HashTable
y algunos otros contenedores asociativos utilizarán este método:
Un código hash es un valor numérico que se usa para insertar e identificar un objeto en una colección basada en hash, como la clase Dictionary <TKey, TValue>, la clase Hashtable o un tipo derivado de la clase DictionaryBase. El método GetHashCode proporciona este código hash para algoritmos que necesitan verificaciones rápidas de la igualdad de objetos.
Entonces, este enfoque tiene grandes limitaciones.
Y aún más , ¿y si quieres construir una biblioteca de propósito general? No solo no puede modificar el código fuente de las clases utilizadas, sino que su comportamiento también es impredecible.
Agradezco que Jon y Simon hayan publicado sus respuestas, y a continuación publicaré un ejemplo de código y una sugerencia sobre el rendimiento.
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();
}
}
En mi prueba, ObjectIDGenerator
lanzará una excepción para quejarse de que hay demasiados objetos cuando se crean 10,000,000 de objetos (10x que en el código anterior) en el ciclo for
.
Además, el resultado de referencia es que la implementación de ConditionalWeakTable
es 1.8 veces más rápida que la implementación de ObjectIDGenerator
.
Tendría que asignar ese identificador usted mismo, manualmente, ya sea dentro de la instancia o externamente.
Para los registros relacionados con una base de datos, la clave principal puede ser útil (pero aún puede obtener duplicados). Alternativamente, use un Guid
o mantenga su propio contador, asignando usando Interlocked.Increment
(y haga que sea lo suficientemente grande como para que no se desborde).
RuntimeHelpers.GetHashCode()
puede ayudar ( MSDN ).