Diccionario de claves compuestas

90

Tengo algunos objetos en List, digamos List<MyClass>y MyClass tiene varias propiedades. Me gustaría crear un índice de la lista basado en 3 propiedades de MyClass. En este caso, 2 de las propiedades son int y una propiedad es una fecha y hora.

Básicamente, me gustaría poder hacer algo como:

Dictionary< CompositeKey , MyClass > MyClassListIndex = Dictionary< CompositeKey , MyClass >();
//Populate dictionary with items from the List<MyClass> MyClassList
MyClass aMyClass = Dicitonary[(keyTripletHere)];

A veces creo varios diccionarios en una lista para indexar diferentes propiedades de las clases que contiene. Sin embargo, no estoy seguro de cuál es la mejor manera de manejar las claves compuestas. Consideré hacer una suma de comprobación de los tres valores, pero esto corre el riesgo de colisiones.

AaronLS
fuente
2
¿Por qué no usas Tuples? Ellos hacen toda la composición por ti.
Eldritch Conundrum
21
No sé cómo responder a eso. Hace esa pregunta como si hubiera asumido que estoy evitando deliberadamente las tuplas.
AaronLS
6
Lo siento, lo reescribí como una respuesta más detallada.
Eldritch Conundrum
1
Antes de implementar una clase personalizada, lea sobre Tuple (como lo sugiere Eldritch Conundrum): msdn.microsoft.com/en-us/library/system.tuple.aspx . Son más fáciles de cambiar y te ahorrarán la creación de clases personalizadas.
OSH

Respuestas:

105

Deberías usar tuplas. Son equivalentes a una clase CompositeKey, pero Equals () y GetHashCode () ya están implementados.

var myClassIndex = new Dictionary<Tuple<int, bool, string>, MyClass>();
//Populate dictionary with items from the List<MyClass> MyClassList
foreach (var myObj in myClassList)
    myClassIndex.Add(Tuple.Create(myObj.MyInt, myObj.MyBool, myObj.MyString), myObj);
MyClass myObj = myClassIndex[Tuple.Create(4, true, "t")];

O usando System.Linq

var myClassIndex = myClassList.ToDictionary(myObj => Tuple.Create(myObj.MyInt, myObj.MyBool, myObj.MyString));
MyClass myObj = myClassIndex[Tuple.Create(4, true, "t")];

A menos que necesite personalizar el cálculo del hash, es más sencillo usar tuplas.

Si hay muchas propiedades que desea incluir en la clave compuesta, el nombre del tipo Tuple puede volverse bastante largo, pero puede acortar el nombre creando su propia clase derivada de Tuple <...>.


** editado en 2017 **

Hay una nueva opción que comienza con C # 7: las tuplas de valor . La idea es la misma, pero la sintaxis es diferente, más ligera:

El tipo se Tuple<int, bool, string>convierte en (int, bool, string)y el valor se Tuple.Create(4, true, "t")convierte en (4, true, "t").

Con tuplas de valor, también es posible nombrar los elementos. Tenga en cuenta que los rendimientos son ligeramente diferentes, por lo que es posible que desee realizar una evaluación comparativa si son importantes para usted.

Enigma sobrenatural
fuente
4
Tuple no es un buen candidato para una clave, ya que crea una gran cantidad de colisiones hash. stackoverflow.com/questions/12657348/…
paparazzo
1
@Blam KeyValuePair<K,V>y otras estructuras tienen una función hash predeterminada que se sabe que es incorrecta (consulte stackoverflow.com/questions/3841602/… para obtener más detalles). Tuple<>sin embargo, no es un ValueType, y su función hash predeterminada al menos usará todos los campos. Dicho esto, si el principal problema de su código son las colisiones, implemente una versión optimizada GetHashCode()que se adapte a sus datos.
Eldritch Conundrum
1
Aunque Tuple no es un ValueType de mis pruebas, sufre de muchas colisiones
paparazzo
5
Creo que esta respuesta está desactualizada ahora que tenemos ValueTuples. Tienen una sintaxis más agradable en C #, y parecen hacer GetHashCode dos veces más rápido que Tuples
Lucian Wischik
3
@LucianWischik Gracias, he actualizado la respuesta para mencionarlos.
Eldritch Conundrum
22

La mejor manera que se me ocurre es crear una estructura CompositeKey y asegurarme de anular los métodos GetHashCode () y Equals () para garantizar la velocidad y precisión al trabajar con la colección:

class Program
{
    static void Main(string[] args)
    {
        DateTime firstTimestamp = DateTime.Now;
        DateTime secondTimestamp = firstTimestamp.AddDays(1);

        /* begin composite key dictionary populate */
        Dictionary<CompositeKey, string> compositeKeyDictionary = new Dictionary<CompositeKey, string>();

        CompositeKey compositeKey1 = new CompositeKey();
        compositeKey1.Int1 = 11;
        compositeKey1.Int2 = 304;
        compositeKey1.DateTime = firstTimestamp;

        compositeKeyDictionary[compositeKey1] = "FirstObject";

        CompositeKey compositeKey2 = new CompositeKey();
        compositeKey2.Int1 = 12;
        compositeKey2.Int2 = 9852;
        compositeKey2.DateTime = secondTimestamp;

        compositeKeyDictionary[compositeKey2] = "SecondObject";
        /* end composite key dictionary populate */

        /* begin composite key dictionary lookup */
        CompositeKey compositeKeyLookup1 = new CompositeKey();
        compositeKeyLookup1.Int1 = 11;
        compositeKeyLookup1.Int2 = 304;
        compositeKeyLookup1.DateTime = firstTimestamp;

        Console.Out.WriteLine(compositeKeyDictionary[compositeKeyLookup1]);

        CompositeKey compositeKeyLookup2 = new CompositeKey();
        compositeKeyLookup2.Int1 = 12;
        compositeKeyLookup2.Int2 = 9852;
        compositeKeyLookup2.DateTime = secondTimestamp;

        Console.Out.WriteLine(compositeKeyDictionary[compositeKeyLookup2]);
        /* end composite key dictionary lookup */
    }

    struct CompositeKey
    {
        public int Int1 { get; set; }
        public int Int2 { get; set; }
        public DateTime DateTime { get; set; }

        public override int GetHashCode()
        {
            return Int1.GetHashCode() ^ Int2.GetHashCode() ^ DateTime.GetHashCode();
        }

        public override bool Equals(object obj)
        {
            if (obj is CompositeKey)
            {
                CompositeKey compositeKey = (CompositeKey)obj;

                return ((this.Int1 == compositeKey.Int1) &&
                        (this.Int2 == compositeKey.Int2) &&
                        (this.DateTime == compositeKey.DateTime));
            }

            return false;
        }
    }
}

Un artículo de MSDN sobre GetHashCode ():

http://msdn.microsoft.com/en-us/library/system.object.gethashcode.aspx

Allen E. Scharfenberg
fuente
No creo que sea 100% seguro que sea un código hash único, solo que es muy probable.
Hans Olsson
¡Eso puede muy bien ser cierto! De acuerdo con el artículo de MSDN vinculado, esa es la forma recomendada de invalidar GetHashCode (). Sin embargo, dado que no uso muchas claves compuestas en mi trabajo diario, no puedo decirlo con certeza.
Allen E. Scharfenberg
4
Si. Si desmonta Dictionary.FindEntry () con Reflector, verá que se prueban tanto el código hash como la igualdad total. El código hash se prueba primero y, si falla, cortocircuita la condición sin verificar la igualdad total. Si pasa el hash, la igualdad también se prueba.
Jason Kleban
1
Y sí, los iguales también deben anularse para que coincidan. Incluso si hiciera que GetHashCode () devuelva 0 para cualquier instancia, el Diccionario aún funcionaría, simplemente sería más lento.
Jason Kleban
2
El tipo integrado Tuple implementa la combinación de hash como '(h1 << 5) + h1 ^ h2' en lugar de su 'h1 ^ h2'. Supongo que lo hacen para evitar colisiones cada vez que los dos objetos a hash tienen el mismo valor.
Eldritch Conundrum
13

¿Qué tal Dictionary<int, Dictionary<int, Dictionary<DateTime, MyClass>>>?

Esto le permitiría hacer:

MyClass item = MyData[8][23923][date];
Jason Kleban
fuente
1
esto creará mucho más objeto que usar una estructura o clase CompositeKey. y también será más lento ya que se utilizarán dos niveles de búsqueda.
Ian Ringrose
Creo que es el mismo número de comparaciones, no veo cómo habría muchos más objetos, la clave compuesta todavía necesita una clave, y los valores u objetos de los componentes y un dictado para contenerlos. De esta manera anidada, no necesita la clave contenedora para cada objeto / valor, un dict adicional para cada nivel de nido adicional. ¿Qué piensas?
Jason Kleban
9
Basado en mi evaluación comparativa, que probé con claves con 2 y 3 partes: una solución de diccionario anidado es 3-4 veces más rápida que usar un enfoque de clave compuesta de tuplas. Sin embargo, el enfoque de tuplas es mucho más fácil / ordenado.
RickL
5
@RickL Puedo confirmar esos puntos de referencia, usamos un tipo en nuestro código base, llamado CompositeDictionary<TKey1, TKey2, TValue>(etc.) que simplemente hereda de Dictionary<TKey1, Dictionary<TKey2, TValue>>(o de cuantos diccionarios anidados se requieran. Sin implementar el tipo completo desde cero nosotros mismos (en lugar de hacer trampa usando diccionarios anidados o tipos para contener las claves) esto es lo más rápido que obtenemos.
Adam Houldsworth
1
El enfoque de dictado anidado debería ser más rápido solo en la mitad (?) De los casos en los que los datos no están presentes, ya que los diccionarios intermedios pueden omitir el cálculo y la comparación del código hash completo. En presencia de datos, debería ser más lento ya que las operaciones básicas como Agregar, Contiene, etc.deben realizarse tres veces. Estoy seguro de que el margen con el enfoque de tupla es superado en algunos de los puntos de referencia mencionados anteriormente y se trata del detalle de implementación de las tuplas .NET, que es bastante pobre considerando la penalización de boxeo que conlleva para los tipos de valor. Un triplete implementado correctamente es con lo que iría, considerando también la memoria
nawfal
12

Puede almacenarlos en una estructura y usar eso como clave:

struct CompositeKey
{
  public int value1;
  public int value2;
  public DateTime value3;
}

Enlace para obtener el código hash: http://msdn.microsoft.com/en-us/library/system.valuetype.gethashcode.aspx

kemiller2002
fuente
Estoy atascado en .NET 3.5, así que no tengo acceso a Tuplelos correos electrónicos, ¡así que esta es una buena solución!
aarona
Me sorprende que esto no tenga más votos a favor. Es una solución simple que es más legible que una tupla.
Marcos
1
Según msdn, esto funciona bien, si ningún campo es de tipo de referencia, de lo contrario, usa la reflexión para la igualdad.
Gregor Slavec
@Mark El problema con una estructura es que su implementación predeterminada GetHashCode () en realidad no garantiza el uso de todos los campos de la estructura (lo que lleva a un rendimiento deficiente del diccionario), mientras que Tuple ofrece dicha garantía. Lo he probado. Consulte stackoverflow.com/questions/3841602/… para obtener detalles sangrientos.
Eldritch Conundrum
8

Ahora que ha salido VS2017 / C # 7, la mejor respuesta es usar ValueTuple:

// declare:
Dictionary<(string, string, int), MyClass> index;

// populate:
foreach (var m in myClassList) {
  index[(m.Name, m.Path, m.JobId)] = m;
}

// retrieve:
var aMyClass = index[("foo", "bar", 15)];

Elegí declarar el diccionario con un ValueTuple anónimo (string, string, int). Pero podría haberles dado nombres (string name, string path, int id).

Perfwise, el nuevo ValueTuple es más rápido que Tuple en GetHashCodepero más lento en Equals. Creo que necesitarías hacer experimentos completos de un extremo a otro para descubrir cuál es realmente más rápido para tu escenario. Pero la amabilidad de un extremo a otro y la sintaxis del lenguaje de ValueTuple hacen que gane.

// Perf from https://gist.github.com/ljw1004/61bc96700d0b03c17cf83dbb51437a69
//
//              Tuple ValueTuple KeyValuePair
//  Allocation:  160   100        110
//    Argument:   75    80         80    
//      Return:   75   210        210
//        Load:  160   170        320
// GetHashCode:  820   420       2700
//      Equals:  280   470       6800
Lucian Wischik
fuente
Sí, pasé por una gran reescritura solo para que la solución de tipo anónimo explotara en mi cara (no puedo comparar tipos anónimos creados con diferentes ensamblajes). ValueTuple parece ser una solución relativamente elegante al problema de las claves de diccionario compuesto.
Quarkly
5

Inmediatamente me vienen a la mente dos enfoques:

  1. Haz lo que Kevin sugirió y escribe una estructura que te sirva como clave. Asegúrese de hacer que esta estructura se implemente IEquatable<TKey>y anule sus métodos Equalsy GetHashCode*.

  2. Escriba una clase que utilice diccionarios anidados internamente. Algo así como: TripleKeyDictionary<TKey1, TKey2, TKey3, TValue>... esta clase sería internamente tener un miembro de tipo Dictionary<TKey1, Dictionary<TKey2, Dictionary<TKey3, TValue>>>, y expondría métodos tales como this[TKey1 k1, TKey2 k2, TKey3 k3], ContainsKeys(TKey1 k1, TKey2 k2, TKey3 k3), etc.

* Una palabra sobre si Equalses necesario anular el método: si bien es cierto que el Equalsmétodo para una estructura compara el valor de cada miembro de forma predeterminada, lo hace mediante la reflexión, que inherentemente implica costos de rendimiento, y por lo tanto no es muy implementación apropiada para algo que está destinado a ser utilizado como clave en un diccionario (en mi opinión, de todos modos). Según la documentación de MSDN sobre ValueType.Equals:

La implementación predeterminada del método Equals usa la reflexión para comparar los campos correspondientes de obj y esta instancia. Anule el método Equals para un tipo en particular para mejorar el rendimiento del método y representar más de cerca el concepto de igualdad para el tipo.

Dan Tao
fuente
Con respecto a 1, no creo que deba anular Equals y GetHashcode, la implementación predeterminada de Equals verificará automáticamente la igualdad en todos los campos que creo que deberían estar bien en esta estructura.
Hans Olsson
@ho: Puede que no sea necesario , pero recomiendo encarecidamente hacerlo para cualquier estructura que vaya a servir como clave. Ver mi edición.
Dan Tao
3

Si la clave es parte de la clase, utilice KeyedCollection.
Es un lugar Dictionarydonde la clave se deriva del objeto.
Debajo de las sábanas está el Diccionario.
No es necesario repetir la clave en Keyy Value.
Por qué arriesgarse, la clave no es la misma en Keyel Value.
No es necesario que duplique la misma información en la memoria.

Clase KeyedCollection

Indexador para exponer la clave compuesta

    using System.Collections.ObjectModel;

    namespace IntIntKeyedCollection
    {
        class Program
        {
            static void Main(string[] args)
            {
                Int32Int32DateO iid1 = new Int32Int32DateO(0, 1, new DateTime(2007, 6, 1, 8, 30, 52));
                Int32Int32DateO iid2 = new Int32Int32DateO(0, 1, new DateTime(2007, 6, 1, 8, 30, 52));
                if (iid1 == iid2) Console.WriteLine("same");
                if (iid1.Equals(iid2)) Console.WriteLine("equals");
                // that are equal but not the same I don't override = so I have both features

                Int32Int32DateCollection int32Int32DateCollection = new Int32Int32DateCollection();
                // dont't have to repeat the key like Dictionary
                int32Int32DateCollection.Add(new Int32Int32DateO(0, 0, new DateTime(2008, 5, 1, 8, 30, 52)));
                int32Int32DateCollection.Add(new Int32Int32DateO(0, 1, new DateTime(2008, 6, 1, 8, 30, 52)));
                int32Int32DateCollection.Add(iid1);
                //this would thow a duplicate key error
                //int32Int32DateCollection.Add(iid2);
                //this would thow a duplicate key error
                //int32Int32DateCollection.Add(new Int32Int32DateO(0, 1, new DateTime(2008, 6, 1, 8, 30, 52)));
                Console.WriteLine("count");
                Console.WriteLine(int32Int32DateCollection.Count.ToString());
                // reference by ordinal postion (note the is not the long key)
                Console.WriteLine("oridinal");
                Console.WriteLine(int32Int32DateCollection[0].GetHashCode().ToString());
                // reference by index
                Console.WriteLine("index");
                Console.WriteLine(int32Int32DateCollection[0, 1, new DateTime(2008, 6, 1, 8, 30, 52)].GetHashCode().ToString());
                Console.WriteLine("foreach");
                foreach (Int32Int32DateO iio in int32Int32DateCollection)
                {
                    Console.WriteLine(string.Format("HashCode {0} Int1 {1} Int2 {2} DateTime {3}", iio.GetHashCode(), iio.Int1, iio.Int2, iio.Date1));
                }
                Console.WriteLine("sorted by date");
                foreach (Int32Int32DateO iio in int32Int32DateCollection.OrderBy(x => x.Date1).ThenBy(x => x.Int1).ThenBy(x => x.Int2))
                {
                    Console.WriteLine(string.Format("HashCode {0} Int1 {1} Int2 {2} DateTime {3}", iio.GetHashCode(), iio.Int1, iio.Int2, iio.Date1));
                }
                Console.ReadLine();
            }
            public class Int32Int32DateCollection : KeyedCollection<Int32Int32DateS, Int32Int32DateO>
            {
                // This parameterless constructor calls the base class constructor 
                // that specifies a dictionary threshold of 0, so that the internal 
                // dictionary is created as soon as an item is added to the  
                // collection. 
                // 
                public Int32Int32DateCollection() : base(null, 0) { }

                // This is the only method that absolutely must be overridden, 
                // because without it the KeyedCollection cannot extract the 
                // keys from the items.  
                // 
                protected override Int32Int32DateS GetKeyForItem(Int32Int32DateO item)
                {
                    // In this example, the key is the part number. 
                    return item.Int32Int32Date;
                }

                //  indexer 
                public Int32Int32DateO this[Int32 Int1, Int32 Int2, DateTime Date1]
                {
                    get { return this[new Int32Int32DateS(Int1, Int2, Date1)]; }
                }
            }

            public struct Int32Int32DateS
            {   // required as KeyCollection Key must be a single item
                // but you don't really need to interact with Int32Int32DateS directly
                public readonly Int32 Int1, Int2;
                public readonly DateTime Date1;
                public Int32Int32DateS(Int32 int1, Int32 int2, DateTime date1)
                { this.Int1 = int1; this.Int2 = int2; this.Date1 = date1; }
            }
            public class Int32Int32DateO : Object
            {
                // implement other properties
                public Int32Int32DateS Int32Int32Date { get; private set; }
                public Int32 Int1 { get { return Int32Int32Date.Int1; } }
                public Int32 Int2 { get { return Int32Int32Date.Int2; } }
                public DateTime Date1 { get { return Int32Int32Date.Date1; } }

                public override bool Equals(Object obj)
                {
                    //Check for null and compare run-time types.
                    if (obj == null || !(obj is Int32Int32DateO)) return false;
                    Int32Int32DateO item = (Int32Int32DateO)obj;
                    return (this.Int32Int32Date.Int1 == item.Int32Int32Date.Int1 &&
                            this.Int32Int32Date.Int2 == item.Int32Int32Date.Int2 &&
                            this.Int32Int32Date.Date1 == item.Int32Int32Date.Date1);
                }
                public override int GetHashCode()
                {
                    return (((Int64)Int32Int32Date.Int1 << 32) + Int32Int32Date.Int2).GetHashCode() ^ Int32Int32Date.GetHashCode();
                }
                public Int32Int32DateO(Int32 Int1, Int32 Int2, DateTime Date1)
                {
                    Int32Int32DateS int32Int32Date = new Int32Int32DateS(Int1, Int2, Date1);
                    this.Int32Int32Date = int32Int32Date;
                }
            }
        }
    }

En cuanto al uso del tipo de valor fpr, la clave que Microsoft recomienda específicamente no hacerlo.

ValueType.GetHashCode

Tuple técnicamente no es un tipo de valor, pero sufre el mismo síntoma (colisiones hash) y no es un buen candidato para una clave.

paparazzo
fuente
+1 para una respuesta más correcta. Sorprendido que nadie lo haya mencionado antes. De hecho, dependiendo de cómo OP intente usar la estructura, una opción HashSet<T>apropiada también IEqualityComparer<T>sería una opción. Por cierto, creo que su respuesta atraerá votos si puede cambiar los nombres de sus clases y otros nombres de miembros :)
nawfal
2

Puedo sugerir una alternativa: un objeto anónimo. Es el mismo que usamos en el método GroupBy LINQ con múltiples claves.

var dictionary = new Dictionary<object, string> ();
dictionary[new { a = 1, b = 2 }] = "value";

Puede parecer extraño, pero he evaluado Tuple.GetHashCode y los nuevos métodos {a = 1, b = 2} .GetHashCode y los objetos anónimos ganan en mi máquina en .NET 4.5.1:

Objeto: 89,1732 ms para 10000 llamadas en 1000 ciclos

Tupla: 738,4475 ms para 10000 llamadas en 1000 ciclos

Michael Logutov
fuente
Dios mío, esta alternativa nunca estuvo en mi mente ... No sé si se comportará bien si usa un tipo complejo como clave compuesta.
Gabriel Espinoza
Si simplemente pasa un objeto (en lugar de uno anónimo), se utilizará el resultado del método GetHashCode de este objeto. Si lo usa así dictionary[new { a = my_obj, b = 2 }], el código hash resultante será una combinación de my_obj.GetHashCode y ((Int32) 2) .GetHashCode.
Michael Logutov
¡NO USE ESTE MÉTODO! Los diferentes ensamblados crean diferentes nombres para tipos anónimos. Si bien le parece anónimo, detrás de escena hay una clase concreta creada y dos objetos de dos clases diferentes no serán iguales al operador predeterminado.
Quarkly
¿Y qué importa eso en este caso?
Michael Logutov
0

Otra solución a las ya mencionadas sería almacenar algún tipo de lista de todas las claves generadas hasta ahora y cuando se genera un nuevo objeto generas su código hash (solo como punto de partida), verifica si ya está en la lista, si es, luego agregue algún valor aleatorio, etc.hasta que tenga una clave única, luego almacene esa clave en el objeto mismo y en la lista y devuélvala como la clave en todo momento.

Hans Olsson
fuente