¿Claves duplicadas en diccionarios .NET?

256

¿Hay alguna clase de diccionario en la biblioteca de clases base .NET que permita usar claves duplicadas? La única solución que he encontrado es crear, por ejemplo, una clase como:

Dictionary<string, List<object>>

Pero esto es bastante irritante para usar realmente. En Java, creo que un MultiMap logra esto, pero no puede encontrar un análogo en .NET.

Caracol mecánico
fuente
3
¿Cómo es esta clave duplicada, son valores duplicados (la Lista), ¿verdad?
Shamim Hafiz
1
@ShamimHafiz, no, los valores no necesitan ser duplicados. Si tiene que almacenar duplicados { a, 1 }y { a, 2 }en una tabla hash donde aestá la clave, una alternativa es tener { a, [1, 2] }.
nawfal
1
En realidad, creo que lo que realmente se quiere aquí es una colección en la que cada clave se pueda asignar a uno o más valores. Creo que la expresión "claves duplicadas" realmente no transmite esto.
DavidRR
Para referencia futura, debe considerar mantener 1 clave simplemente agregando los valores en lugar de agregar las mismas claves una y otra vez.
Ya Wang
Si tanto las claves como los valores son cadenas, existe NameValueCollection (que puede asociar múltiples valores de cadena con cada clave de cadena).
AnorZaken

Respuestas:

229

Si está utilizando .NET 3.5, use la Lookupclase

EDITAR: generalmente crea un Lookupuso Enumerable.ToLookup. Esto supone que no es necesario cambiarlo después, pero normalmente encuentro que es lo suficientemente bueno.

Si eso no funciona para usted, no creo que haya nada en el marco que lo ayude, y usar el diccionario es tan bueno como parece :(

Jon Skeet
fuente
Gracias por el aviso en Lookup. Ofrece una excelente manera de particionar (agrupar) resultados de una consulta linq que no son criterios de orden estándar.
Robert Paulson
3
@ Josh: Utiliza Enumerable.ToLookup para crear uno.
Jon Skeet
29
Palabra de precaución : Lookupno es serializable
SliverNinja - MSFT
1
¿Cómo debemos agregar elementos a esa búsqueda?
moldovanu
171

La clase Lista en realidad funciona bastante bien para colecciones de clave / valor que contienen duplicados en los que desea iterar sobre la colección. Ejemplo:

List<KeyValuePair<string, string>> list = new List<KeyValuePair<string, string>>();

// add some values to the collection here

for (int i = 0;  i < list.Count;  i++)
{
    Print(list[i].Key, list[i].Value);
}
John Saunders
fuente
31
Esta solución funciona funcionalmente, pero la implementación de List no tiene conocimiento de la clave o el valor y no puede optimizar la búsqueda de claves
Spencer Rose
41

Aquí hay una forma de hacerlo con List <KeyValuePair <string, string>>

public class ListWithDuplicates : List<KeyValuePair<string, string>>
{
    public void Add(string key, string value)
    {
        var element = new KeyValuePair<string, string>(key, value);
        this.Add(element);
    }
}

var list = new ListWithDuplicates();
list.Add("k1", "v1");
list.Add("k1", "v2");
list.Add("k1", "v3");

foreach(var item in list)
{
    string x = string.format("{0}={1}, ", item.Key, item.Value);
}

Salidas k1 = v1, k1 = v2, k1 = v3

Héctor Correa
fuente
Funciona muy bien en mi escenario y también es fácil de usar.
user6121177
21

Si usa cadenas como claves y valores, puede usar System.Collections.Specialized.NameValueCollection , que devolverá una matriz de valores de cadena a través del método GetValues ​​(clave de cadena).

Mate
fuente
66
NameValueCollection no permite múltiples claves.
Jenny O'Reilly
@ Jenny O'Reilly: Claro, puedes agregar claves duplicadas.
isHuman
Estrictamente hablando, @ JennyO'Reilly es correcto, ya que los comentarios en la página vinculada de MSDN establecen claramente: esta clase almacena múltiples valores de cadena bajo una sola clave .
Ronald
Permitirá pero devolverá múltiples valores, intenté usar el índice y la clave.
user6121177
18

Acabo de encontrar la biblioteca PowerCollections que incluye, entre otras cosas, una clase llamada MultiDictionary. Esto envuelve perfectamente este tipo de funcionalidad.


fuente
14

Nota muy importante sobre el uso de la búsqueda:

Puede crear una instancia de a Lookup(TKey, TElement)llamando ToLookupa un objeto que implementeIEnumerable(T)

No hay un constructor público para crear una nueva instancia de a Lookup(TKey, TElement). Además, los Lookup(TKey, TElement)objetos son inmutables, es decir, no puede agregar o eliminar elementos o claves de un Lookup(TKey, TElement)objeto después de que se haya creado.

(de MSDN)

Creo que esto sería un espectáculo para la mayoría de los usos.

TheSoftwareJedi
fuente
66
Puedo pensar en muy pocos usos en los que esto sería un obstáculo para el espectáculo. Pero entonces, creo que los objetos inmutables son geniales.
Joel Mueller
1
@JoelMueller Puedo pensar en muchos casos en los que esto es un obstáculo para el espectáculo. Tener que volver a crear un diccionario para agregar un elemento no es una solución particularmente eficiente ...
reirab 02 de
12

Creo que algo así List<KeyValuePair<object, object>>haría el trabajo.

MADMapa
fuente
¿Cómo buscas algo en esa lista por su clave?
Wayne Bloss
Debe iterar a través de él: pero no estaba al tanto de la clase LookUp de .NET 3.5: tal vez esto sea más útil para buscar en su contenido.
MADMap
2
@wizlib: la única forma es recorrer la lista, que no es tan eficiente como el hash. -1
petr k.
@petrk. Eso realmente depende de cuáles sean sus datos. Utilicé esta implementación porque tenía muy pocas claves únicas y no quería incurrir en la sobrecarga de las colisiones hash. +1
Evan M
9

Si está usando> = .NET 4, entonces puede usar TupleClass:

// declaration
var list = new List<Tuple<string, List<object>>>();

// to add an item to the list
var item = Tuple<string, List<object>>("key", new List<object>);
list.Add(item);

// to iterate
foreach(var i in list)
{
    Console.WriteLine(i.Item1.ToString());
}

fuente
Esto parece una List<KeyValuePair<key, value>>solución como la anterior. ¿Me equivoco?
Nathan Goings
6

Es bastante fácil "rodar su propia versión" de un diccionario que permite entradas de "clave duplicada". Aquí hay una implementación simple y aproximada. Es posible que desee agregar soporte para básicamente la mayoría (si no todos) en IDictionary<T>.

public class MultiMap<TKey,TValue>
{
    private readonly Dictionary<TKey,IList<TValue>> storage;

    public MultiMap()
    {
        storage = new Dictionary<TKey,IList<TValue>>();
    }

    public void Add(TKey key, TValue value)
    {
        if (!storage.ContainsKey(key)) storage.Add(key, new List<TValue>());
        storage[key].Add(value);
    }

    public IEnumerable<TKey> Keys
    {
        get { return storage.Keys; }
    }

    public bool ContainsKey(TKey key)
    {
        return storage.ContainsKey(key);
    }

    public IList<TValue> this[TKey key]
    {
        get
        {
            if (!storage.ContainsKey(key))
                throw new KeyNotFoundException(
                    string.Format(
                        "The given key {0} was not found in the collection.", key));
            return storage[key];
        }
    }
}

Un ejemplo rápido sobre cómo usarlo:

const string key = "supported_encodings";
var map = new MultiMap<string,Encoding>();
map.Add(key, Encoding.ASCII);
map.Add(key, Encoding.UTF8);
map.Add(key, Encoding.Unicode);

foreach (var existingKey in map.Keys)
{
    var values = map[existingKey];
    Console.WriteLine(string.Join(",", values));
}
ChristopheD
fuente
3

NameValueCollection admite múltiples valores de cadena bajo una clave (que también es una cadena), pero es el único ejemplo que conozco.

Tiendo a crear construcciones similares a la de su ejemplo cuando me encuentro con situaciones en las que necesito ese tipo de funcionalidad.

ckramer
fuente
3

Al usar la List<KeyValuePair<string, object>>opción, puede usar LINQ para hacer la búsqueda:

List<KeyValuePair<string, object>> myList = new List<KeyValuePair<string, object>>();
//fill it here
var q = from a in myList Where a.Key.Equals("somevalue") Select a.Value
if(q.Count() > 0){ //you've got your value }
Greg
fuente
2
Sí, pero eso no lo hace más rápido (aún sin hashing)
Haukman
3

Desde el nuevo C # (creo que es de 7.0), también puede hacer algo como esto:

var duplicatedDictionaryExample = new List<(string Key, string Value)> { ("", "") ... }

y lo está utilizando como una lista estándar, pero con dos valores nombrados como desee

foreach(var entry in duplicatedDictionaryExample)
{ 
    // do something with the values
    entry.Key;
    entry.Value;
}
reniasa
fuente
2

¿Quieres decir congruente y no un duplicado real? De lo contrario, una tabla hash no podría funcionar.

Congruente significa que dos claves separadas pueden dividir en hash al valor equivalente, pero las claves no son iguales.

Por ejemplo: supongamos que la función hash de su tabla hash era simplemente hashval = key mod 3. Tanto 1 como 4 se asignan a 1, pero son valores diferentes. Aquí es donde entra en juego tu idea de una lista.

Cuando necesita buscar 1, ese valor se divide en 1, la lista se recorre hasta que se encuentra la Clave = 1.

Si permitiera insertar claves duplicadas, no podría diferenciar qué claves se asignan a qué valores.

Nicholas Mancuso
fuente
2
Una tabla hash ya maneja claves que se convierten en hash con el mismo valor (esto se llama colisión). Me refiero a una situación en la que desea asignar varios valores a la misma clave exacta.
2

La forma en que uso es solo un

Dictionary<string, List<string>>

De esta manera, tiene una sola tecla que contiene una lista de cadenas.

Ejemplo:

List<string> value = new List<string>();
if (dictionary.Contains(key)) {
     value = dictionary[key];
}
value.Add(newValue);
Stefan Mielke
fuente
Eso es lindo y limpio.
Jerry Nixon
Esta es una gran solución para manejar mi caso de uso.
dub
1

Me topé con esta publicación en busca de la misma respuesta, y no encontré ninguna, así que preparé una solución de ejemplo básica usando una lista de diccionarios, anulando el operador [] para agregar un nuevo diccionario a la lista cuando todos los demás tienen un clave dada (conjunto) y devuelve una lista de valores (get).
Es feo e ineficiente, SOLO obtiene / establece por clave, y siempre devuelve una lista, pero funciona:

 class DKD {
        List<Dictionary<string, string>> dictionaries;
        public DKD(){
            dictionaries = new List<Dictionary<string, string>>();}
        public object this[string key]{
             get{
                string temp;
                List<string> valueList = new List<string>();
                for (int i = 0; i < dictionaries.Count; i++){
                    dictionaries[i].TryGetValue(key, out temp);
                    if (temp == key){
                        valueList.Add(temp);}}
                return valueList;}
            set{
                for (int i = 0; i < dictionaries.Count; i++){
                    if (dictionaries[i].ContainsKey(key)){
                        continue;}
                    else{
                        dictionaries[i].Add(key,(string) value);
                        return;}}
                dictionaries.Add(new Dictionary<string, string>());
                dictionaries.Last()[key] =(string)value;
            }
        }
    }
Sintrínseco
fuente
1

Cambié la respuesta de @Hector Correa a una extensión con tipos genéricos y también le agregué un TryGetValue personalizado.

  public static class ListWithDuplicateExtensions
  {
    public static void Add<TKey, TValue>(this List<KeyValuePair<TKey, TValue>> collection, TKey key, TValue value)
    {
      var element = new KeyValuePair<TKey, TValue>(key, value);
      collection.Add(element);
    }

    public static int TryGetValue<TKey, TValue>(this List<KeyValuePair<TKey, TValue>> collection, TKey key, out IEnumerable<TValue> values)
    {
      values = collection.Where(pair => pair.Key.Equals(key)).Select(pair => pair.Value);
      return values.Count();
    }
  }
kjhf
fuente
0

Este es un diccionario simultáneo de remolque, creo que esto te ayudará:

public class HashMapDictionary<T1, T2> : System.Collections.IEnumerable
{
    private System.Collections.Concurrent.ConcurrentDictionary<T1, List<T2>> _keyValue = new System.Collections.Concurrent.ConcurrentDictionary<T1, List<T2>>();
    private System.Collections.Concurrent.ConcurrentDictionary<T2, List<T1>> _valueKey = new System.Collections.Concurrent.ConcurrentDictionary<T2, List<T1>>();

    public ICollection<T1> Keys
    {
        get
        {
            return _keyValue.Keys;
        }
    }

    public ICollection<T2> Values
    {
        get
        {
            return _valueKey.Keys;
        }
    }

    public int Count
    {
        get
        {
            return _keyValue.Count;
        }
    }

    public bool IsReadOnly
    {
        get
        {
            return false;
        }
    }

    public List<T2> this[T1 index]
    {
        get { return _keyValue[index]; }
        set { _keyValue[index] = value; }
    }

    public List<T1> this[T2 index]
    {
        get { return _valueKey[index]; }
        set { _valueKey[index] = value; }
    }

    public void Add(T1 key, T2 value)
    {
        lock (this)
        {
            if (!_keyValue.TryGetValue(key, out List<T2> result))
                _keyValue.TryAdd(key, new List<T2>() { value });
            else if (!result.Contains(value))
                result.Add(value);

            if (!_valueKey.TryGetValue(value, out List<T1> result2))
                _valueKey.TryAdd(value, new List<T1>() { key });
            else if (!result2.Contains(key))
                result2.Add(key);
        }
    }

    public bool TryGetValues(T1 key, out List<T2> value)
    {
        return _keyValue.TryGetValue(key, out value);
    }

    public bool TryGetKeys(T2 value, out List<T1> key)
    {
        return _valueKey.TryGetValue(value, out key);
    }

    public bool ContainsKey(T1 key)
    {
        return _keyValue.ContainsKey(key);
    }

    public bool ContainsValue(T2 value)
    {
        return _valueKey.ContainsKey(value);
    }

    public void Remove(T1 key)
    {
        lock (this)
        {
            if (_keyValue.TryRemove(key, out List<T2> values))
            {
                foreach (var item in values)
                {
                    var remove2 = _valueKey.TryRemove(item, out List<T1> keys);
                }
            }
        }
    }

    public void Remove(T2 value)
    {
        lock (this)
        {
            if (_valueKey.TryRemove(value, out List<T1> keys))
            {
                foreach (var item in keys)
                {
                    var remove2 = _keyValue.TryRemove(item, out List<T2> values);
                }
            }
        }
    }

    public void Clear()
    {
        _keyValue.Clear();
        _valueKey.Clear();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return _keyValue.GetEnumerator();
    }
}

ejemplos:

public class TestA
{
    public int MyProperty { get; set; }
}

public class TestB
{
    public int MyProperty { get; set; }
}

            HashMapDictionary<TestA, TestB> hashMapDictionary = new HashMapDictionary<TestA, TestB>();

            var a = new TestA() { MyProperty = 9999 };
            var b = new TestB() { MyProperty = 60 };
            var b2 = new TestB() { MyProperty = 5 };
            hashMapDictionary.Add(a, b);
            hashMapDictionary.Add(a, b2);
            hashMapDictionary.TryGetValues(a, out List<TestB> result);
            foreach (var item in result)
            {
                //do something
            }
Ali Yousefi
fuente
0

yo uso esta clase simple:

public class ListMap<T,V> : List<KeyValuePair<T, V>>
{
    public void Add(T key, V value) {
        Add(new KeyValuePair<T, V>(key, value));
    }

    public List<V> Get(T key) {
        return FindAll(p => p.Key.Equals(key)).ConvertAll(p=> p.Value);
    }
}

uso:

var fruits = new ListMap<int, string>();
fruits.Add(1, "apple");
fruits.Add(1, "orange");
var c = fruits.Get(1).Count; //c = 2;
Juan
fuente
0

Puede crear su propio contenedor de diccionario, algo como este, como beneficio adicional, admite un valor nulo como clave:

/// <summary>
/// Dictionary which supports duplicates and null entries
/// </summary>
/// <typeparam name="TKey">Type of key</typeparam>
/// <typeparam name="TValue">Type of items</typeparam>
public class OpenDictionary<TKey, TValue>
{
    private readonly Lazy<List<TValue>> _nullStorage = new Lazy<List<TValue>>(
        () => new List<TValue>());

    private readonly Dictionary<TKey, List<TValue>> _innerDictionary =
        new Dictionary<TKey, List<TValue>>();

    /// <summary>
    /// Get all entries
    /// </summary>
    public IEnumerable<TValue> Values =>
        _innerDictionary.Values
            .SelectMany(x => x)
            .Concat(_nullStorage.Value);

    /// <summary>
    /// Add an item
    /// </summary>
    public OpenDictionary<TKey, TValue> Add(TKey key, TValue item)
    {
        if (ReferenceEquals(key, null))
            _nullStorage.Value.Add(item);
        else
        {
            if (!_innerDictionary.ContainsKey(key))
                _innerDictionary.Add(key, new List<TValue>());

            _innerDictionary[key].Add(item);
        }

        return this;
    }

    /// <summary>
    /// Remove an entry by key
    /// </summary>
    public OpenDictionary<TKey, TValue> RemoveEntryByKey(TKey key, TValue entry)
    {
        if (ReferenceEquals(key, null))
        {
            int targetIdx = _nullStorage.Value.FindIndex(x => x.Equals(entry));
            if (targetIdx < 0)
                return this;

            _nullStorage.Value.RemoveAt(targetIdx);
        }
        else
        {
            if (!_innerDictionary.ContainsKey(key))
                return this;

            List<TValue> targetChain = _innerDictionary[key];
            if (targetChain.Count == 0)
                return this;

            int targetIdx = targetChain.FindIndex(x => x.Equals(entry));
            if (targetIdx < 0)
                return this;

            targetChain.RemoveAt(targetIdx);
        }

        return this;
    }

    /// <summary>
    /// Remove all entries by key
    /// </summary>
    public OpenDictionary<TKey, TValue> RemoveAllEntriesByKey(TKey key)
    {
        if (ReferenceEquals(key, null))
        {
            if (_nullStorage.IsValueCreated)
                _nullStorage.Value.Clear();
        }       
        else
        {
            if (_innerDictionary.ContainsKey(key))
                _innerDictionary[key].Clear();
        }

        return this;
    }

    /// <summary>
    /// Try get entries by key
    /// </summary>
    public bool TryGetEntries(TKey key, out IReadOnlyList<TValue> entries)
    {
        entries = null;

        if (ReferenceEquals(key, null))
        {
            if (_nullStorage.IsValueCreated)
            {
                entries = _nullStorage.Value;
                return true;
            }
            else return false;
        }
        else
        {
            if (_innerDictionary.ContainsKey(key))
            {
                entries = _innerDictionary[key];
                return true;
            }
            else return false;
        }
    }
}

La muestra de uso:

var dictionary = new OpenDictionary<string, int>();
dictionary.Add("1", 1); 
// The next line won't throw an exception; 
dictionary.Add("1", 2);

dictionary.TryGetEntries("1", out List<int> result); 
// result is { 1, 2 }

dictionary.Add(null, 42);
dictionary.Add(null, 24);
dictionary.TryGetEntries(null, out List<int> result); 
// result is { 42, 24 }
Alexander Tolstikov
fuente
¿Puedes dar alguna explicación sobre lo que hace tu código, cómo responde la pregunta y algunos ejemplos de uso?
Enigmatividad
@ Enigmativity, hace exactamente lo que se le pidió, la pregunta era sugerir algún diccionario que admita duplicados, por lo que ofrecí crear un contenedor alrededor del diccionario .net que admitirá esta funcionalidad y, como ejemplo, creé dicho contenedor, como un bono adicional. manejar nulo como clave (incluso si es una mala práctica, seguro) El uso es bastante simple: var dictionary = new OpenDictionary<string, int>(); dictionary.Add("1", 1); // The next line won't throw an exception; dictionary.Add("1", 2); dictionary.TryGetEntries("1", out List<int> result); // result is { 1, 2 }
Alexander Tolstikov
¿Puedes agregar el detalle a tu respuesta?
Enigmatividad
@ Enigmatividad, si te refieres a la respuesta original, entonces seguro
Alexander Tolstikov
-1

U puede definir un método para construir una clave de cadena compuesta cada vez que quiera usar el diccionario. Debe usar este método para construir su clave, por ejemplo:

private string keyBuilder(int key1, int key2)
{
    return string.Format("{0}/{1}", key1, key2);
}

Para usar:

myDict.ContainsKey(keyBuilder(key1, key2))
Alireza Esrari
fuente
-3

Las claves duplicadas rompen el contrato completo del Diccionario. En un diccionario, cada clave es única y se asigna a un solo valor. Si desea vincular un objeto a un número arbitrario de objetos adicionales, la mejor opción podría ser algo similar a un DataSet (en el lenguaje común de una tabla). Coloque sus claves en una columna y sus valores en la otra. Esto es significativamente más lento que un diccionario, pero esa es su compensación por perder la capacidad de trocear los objetos clave.

Ryan
fuente
55
¿No tiene sentido utilizar un diccionario para aumentar el rendimiento? Usar un DataSet no parece mejor que List <KeyValuePair <T, U >>.
Doug
-4

También esto es posible:

Dictionary<string, string[]> previousAnswers = null;

De esta manera, podemos tener claves únicas. Espero que esto funcione para usted.

shan
fuente
El OP solicitó un diccionario que permita claves duplicadas.
Skaparate
-10

Puede agregar las mismas teclas con mayúsculas y minúsculas como:

key1
Clave1
TECLA1
key1
key1
key1

Sé que es una respuesta ficticia, pero funcionó para mí.

usuario4459653
fuente
44
No, no funcionó para ti. Los diccionarios permiten una búsqueda muy rápida, también clasificada como O (1) , por clave. Lo pierde cuando agrega sus claves múltiples con mayúsculas y minúsculas diferentes, porque ¿cómo las recupera? ¿Prueba todas las combinaciones de mayúsculas / minúsculas? No importa cómo lo haga, el rendimiento no será el de la búsqueda normal de un solo diccionario. Esto se suma a otras deficiencias más obvias, como la limitación de valores por clave, dependiendo del número de caracteres en mayúsculas en la clave.
Eugene Beresovsky