La información que doy aquí no es nueva, solo agregué esto para completar.
La idea de este código es bastante simple:
- Los objetos necesitan una identificación única, que no está allí de forma predeterminada. En cambio, tenemos que confiar en la siguiente mejor opción, que es
RuntimeHelpers.GetHashCode
conseguirnos una especie de identificación única.
- Para verificar la unicidad, esto implica que debemos usar
object.ReferenceEquals
- Sin embargo, todavía nos gustaría tener una identificación única, así que agregué una
GUID
, que es por definición única.
- Como no me gusta bloquear todo si no tengo que hacerlo, no uso
ConditionalWeakTable
.
Combinado, eso 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 algo más que hacer 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 el GC recopila un objeto, comprueba si hay referencias al objeto y, si las hay, las recopila. Entonces, si hay un objeto del ConditionalWeakTable
, ¿por qué se recopilará el objeto al que se hace referencia?
ConditionalWeakTable
usa un pequeño truco, que también usan otras estructuras .NET: en lugar de almacenar una referencia al objeto, en realidad almacena un IntPtr. Como 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 se puede almacenar su puntero real. Cuando el GC golpea el objeto para retirarlo, lo desencaja y lo recoge. Sin embargo, eso significaría que obtenemos un recurso anclado, lo cual no es una buena idea si tiene muchos objetos (debido a problemas de fragmentación de la memoria). Probablemente no es así como funciona.
- Cuando el GC mueve un objeto, vuelve a llamar, que luego puede actualizar las referencias. Esta podría ser la forma en que se implementa a juzgar por las llamadas externas
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. IntPtr es un índice o un puntero en esta lista. La lista solo cambia cuando un objeto cambia de generación, momento en el que una simple devolución de llamada puede actualizar los punteros. Si recuerda cómo funciona Mark & Sweep, esto tiene más sentido. No hay fijación y la eliminación es como antes. Creo que así es como funciona
DependentHandle
.
Esta última solución requiere que el tiempo de ejecución no reutilice 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 asumimos 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 momento. Una vez que el objeto comprueba si el objeto está allí, llama a 'Libre', 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 que todas las llamadas ConditionalWeakTable
hacen un "bloqueo" simple que garantiza que esto no suceda.
Otra cosa a tener en cuenta es que la limpieza de las entradas debe realizarse de vez en cuando. Si bien el GC limpiará los objetos reales, las entradas no. Es por eso que ConditionalWeakTable
solo crece en tamaño. Una vez que alcanza un cierto límite (determinado por la probabilidad de colisión en el hash), activa un Resize
, que verifica si los objetos deben limpiarse; si lo hacen, free
se llama en el proceso GC, quitando el IntPtr
mango.
Creo que esta es también la razón por la DependentHandle
que no se expone directamente: no desea meterse con las cosas y obtener una pérdida de memoria como resultado. La siguiente mejor opción para eso es a WeakReference
(que también almacena un en IntPtr
lugar de un objeto), pero desafortunadamente no incluye el aspecto de 'dependencia'.
Lo que queda es que juegues con la mecánica, 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
}