Clés en double dans les dictionnaires .NET?


256

Existe-t-il des classes de dictionnaire dans la bibliothèque de classes de base .NET qui permettent d'utiliser des clés en double? La seule solution que j'ai trouvée est de créer, par exemple, une classe comme:

Dictionary<string, List<object>>

Mais c'est assez irritant à utiliser. En Java, je crois qu'un MultiMap accomplit cela, mais ne trouve pas d'analogue dans .NET.


3
Comment est cette clé en double, ce sont des valeurs en double (la liste), non?
Shamim Hafiz

1
@ShamimHafiz, non, les valeurs n'ont pas besoin d'être des doublons. Si vous devez stocker des doublons { a, 1 }et { a, 2 }dans une table de hachage où aêtre la clé, une alternative est d'avoir { a, [1, 2] }.
nawfal

1
En fait, je crois que ce que l'on veut vraiment ici, c'est une collection où chaque clé peut correspondre à une ou plusieurs valeurs. Je pense que l'expression "clés en double" ne traduit pas vraiment cela.
DavidRR

Pour référence future, vous devriez envisager de conserver 1 clé en y ajoutant simplement les valeurs au lieu d'ajouter plusieurs fois les mêmes clés.
Ya Wang

Si les clés et les valeurs sont des chaînes, il existe NameValueCollection (qui peut associer plusieurs valeurs de chaîne à chaque clé de chaîne).
AnorZaken le

Réponses:


229

Si vous utilisez .NET 3.5, utilisez la Lookupclasse.

EDIT: Vous créez généralement une Lookuputilisation Enumerable.ToLookup. Cela suppose que vous n'avez pas besoin de le changer par la suite - mais je trouve généralement que c'est assez bon.

Si cela ne fonctionne pas pour vous, je ne pense pas qu'il y ait quoi que ce soit dans le cadre qui vous aidera - et l'utilisation du dictionnaire est aussi bonne que possible :(


Merci pour l'avertissement sur Lookup. Il offre un excellent moyen de partitionner (grouper) les résultats d'une requête linq qui ne sont pas des critères de commande standard.
Robert Paulson

3
@Josh: Vous utilisez Enumerable.ToLookup pour en créer un.
Jon Skeet

29
Mise en garde : Lookupn'est pas sérialisable
SliverNinja - MSFT

1
comment ajouter des éléments à cette recherche?
moldovanu

171

La classe List fonctionne en fait assez bien pour les collections clé / valeur contenant des doublons où vous souhaitez parcourir la collection. Exemple:

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

31
Cette solution fonctionne de manière fonctionnelle, mais l'implémentation de List n'a aucune connaissance de la clé ou de la valeur et ne peut pas optimiser la recherche de clés du tout
Spencer Rose

41

Voici une façon de le faire avec 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);
}

Sorties k1 = v1, k1 = v2, k1 = v3


Fonctionne très bien dans mon scénario et aussi facile à utiliser.
user6121177

21

Si vous utilisez des chaînes à la fois comme clés et comme valeurs, vous pouvez utiliser System.Collections.Specialized.NameValueCollection , qui renverra un tableau de valeurs de chaîne via la méthode GetValues ​​(chaîne clé).


6
NameValueCollection n'autorise pas plusieurs clés.
Jenny O'Reilly

@Jenny O'Reilly: Bien sûr, vous pouvez ajouter des clés en double.
isHuman

Strictement parlant, @ JennyO'Reilly est correct, comme l'indique clairement les remarques sur la page MSDN liée: cette classe stocke plusieurs valeurs de chaîne sous une seule clé .
Ronald

Il permettra mais renverra plusieurs valeurs, j'ai essayé d'utiliser l'index et la clé.
user6121177

18

Je viens de découvrir la bibliothèque PowerCollections qui comprend, entre autres, une classe appelée MultiDictionary. Cela enveloppe parfaitement ce type de fonctionnalité.


14

Remarque très importante concernant l'utilisation de la recherche:

Vous pouvez créer une instance d'un Lookup(TKey, TElement)en appelant ToLookupun objet qui implémenteIEnumerable(T)

Il n'y a pas de constructeur public pour créer une nouvelle instance de a Lookup(TKey, TElement). De plus, les Lookup(TKey, TElement)objets sont immuables, c'est-à-dire que vous ne pouvez pas ajouter ou supprimer des éléments ou des clés d'un Lookup(TKey, TElement)objet après sa création.

(à partir de MSDN)

Je pense que ce serait un bouchon d'exposition pour la plupart des utilisations.


6
Je peux penser à très peu d'utilisations où ce serait un bouchon de spectacle. Mais alors, je pense que les objets immuables sont super.
Joel Mueller

1
@JoelMueller Je peux penser à beaucoup de cas où c'est un bouchon d'exposition.
Devoir

12

Je pense que quelque chose comme List<KeyValuePair<object, object>>ça ferait le travail.


Comment recherchez-vous quelque chose dans cette liste par sa clé?
Wayne Bloss, le

Vous devez le parcourir: mais je n'étais pas au courant de la classe LookUp de .NET 3.5: c'est peut-être plus utile pour rechercher dans son contenu.
MADMap

2
@wizlib: La seule façon est de parcourir la liste, ce qui n'est pas aussi efficace que le hachage. -1
petr k.

@petrk. Cela dépend vraiment de vos données. J'ai utilisé cette implémentation parce que j'avais très peu de clés uniques et que je ne voulais pas encourir de frais généraux de collisions de hachage. +1
Evan M

9

Si vous utilisez> = .NET 4, vous pouvez utiliser 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());
}

Cela ressemble à une List<KeyValuePair<key, value>>solution comme celle ci-dessus. Ai-je tort?
Nathan Goings

6

Il est assez facile de "rouler votre propre" version d'un dictionnaire qui permet des entrées de "clé en double". Voici une implémentation simple et approximative. Vous voudrez peut-être envisager d'ajouter la prise en charge de la plupart des applications (sinon toutes) 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 exemple rapide sur la façon de l'utiliser:

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


3

NameValueCollection prend en charge plusieurs valeurs de chaîne sous une seule clé (qui est également une chaîne), mais c'est le seul exemple que je connaisse.

J'ai tendance à créer des constructions similaires à celle de votre exemple lorsque je rencontre des situations où j'ai besoin de ce type de fonctionnalité.


3

Lorsque vous utilisez l' List<KeyValuePair<string, object>>option, vous pouvez utiliser LINQ pour effectuer la recherche:

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 }

2
Oui, mais cela ne le rend pas plus rapide (toujours pas de hachage)
Haukman

3

Depuis le nouveau C # (je crois que c'est de la version 7.0), vous pouvez également faire quelque chose comme ceci:

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

et vous l'utilisez comme une liste standard, mais avec deux valeurs nommées comme vous le souhaitez

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

2

Voulez-vous dire congruent et non un double réel? Sinon, une table de hachage ne pourrait pas fonctionner.

Congruent signifie que deux clés distinctes peuvent hacher à la valeur équivalente, mais les clés ne sont pas égales.

Par exemple: supposons que la fonction de hachage de votre table de hachage était juste hashval = key mod 3. Les 1 et 4 sont mappés sur 1, mais sont des valeurs différentes. C'est là que votre idée de liste entre en jeu.

Lorsque vous devez rechercher 1, cette valeur est hachée à 1, la liste est parcourue jusqu'à ce que la clé = 1 soit trouvée.

Si vous autorisiez l'insertion de clés en double, vous ne seriez pas en mesure de différencier quelles clés correspondent à quelles valeurs.


2
Une table de hachage gère déjà les clés qui arrivent à hacher à la même valeur (cela s'appelle une collision). Je fais référence à une situation où vous souhaitez mapper plusieurs valeurs à la même clé exacte.

2

La façon dont j'utilise est juste un

Dictionary<string, List<string>>

De cette façon, vous avez une seule clé contenant une liste de chaînes.

Exemple:

List<string> value = new List<string>();
if (dictionary.Contains(key)) {
     value = dictionary[key];
}
value.Add(newValue);

C'est agréable et propre.
Jerry Nixon

C'est une excellente solution pour gérer mon cas d'utilisation.
dub stylee

1

Je suis tombé sur ce post à la recherche de la même réponse et n'en ai trouvé aucune, j'ai donc truqué un exemple de solution à nu en utilisant une liste de dictionnaires, remplaçant l'opérateur [] pour ajouter un nouveau dictionnaire à la liste lorsque tous les autres ont un clé donnée (set), et retourne une liste de valeurs (get).
C'est moche et inefficace, il obtient / définit UNIQUEMENT par clé, et il renvoie toujours une liste, mais cela fonctionne:

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

1

J'ai changé la réponse de @Hector Correa en une extension avec des types génériques et y ai également ajouté un TryGetValue personnalisé.

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

0

Ceci est un moyen de remorquage Dictionnaire simultané Je pense que cela vous aidera:

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

exemples:

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
            }

0

j'utilise cette classe 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);
    }
}

usage:

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

0

Vous pouvez créer votre propre enveloppe de dictionnaire, quelque chose comme celui-ci, en prime, il prend en charge la valeur null comme clé:

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

L'échantillon d'utilisation:

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 }

Pouvez-vous nous expliquer ce que fait votre code, comment il répond à la question et quelques exemples d'utilisation?
Énigmativité

@Enigmativity, il fait exactement ce qui a été demandé, la question était de suggérer un dictionnaire qui prend en charge les doublons, j'ai donc proposé de créer un wrapper autour du dictionnaire .net qui soutiendra cette fonctionnalité et, à titre d'exemple, a créé un tel wrapper, en bonus, il peut gérer null comme clé (même si c'est une mauvaise pratique, bien sûr) L'utilisation est assez 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

Pouvez-vous ajouter des détails à votre réponse?
Énigmativité

@Enigmativity, si vous voulez dire la réponse originale, alors bien sûr
Alexander Tolstikov

-1

Vous pouvez définir une méthode pour construire une clé de chaîne composée partout où vous voulez utiliser un dictionnaire, vous devez utiliser cette méthode pour construire votre clé, par exemple:

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

pour l'utilisation:

myDict.ContainsKey(keyBuilder(key1, key2))

-3

Les clés en double rompent l'intégralité du contrat du dictionnaire. Dans un dictionnaire, chaque clé est unique et mappée sur une seule valeur. Si vous souhaitez lier un objet à un nombre arbitraire d'objets supplémentaires, le meilleur pari pourrait être quelque chose de semblable à un DataSet (dans le langage courant une table). Mettez vos clés dans une colonne et vos valeurs dans l'autre. C'est beaucoup plus lent qu'un dictionnaire, mais c'est votre compromis pour perdre la capacité de hacher les objets clés.


5
N'est-ce pas tout l'intérêt d'utiliser un dictionnaire pour le gain de performances? L'utilisation d'un DataSet ne semble pas meilleure qu'une List <KeyValuePair <T, U >>.
Doug

-4

Cela est également possible:

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

De cette façon, nous pouvons avoir des clés uniques. J'espère que cela fonctionne pour toi.


L'OP a demandé un dictionnaire qui autorise les clés en double.
Skaparate

-10

Vous pouvez ajouter les mêmes clés avec différents cas comme:

key1
Key1
KEY1
KeY1
kEy1
keY1

Je sais que c'est une réponse fictive, mais ça a marché pour moi.


4
Non, cela n'a pas fonctionné pour vous. Les dictionnaires permettent une recherche très rapide - également classée O (1) - par clé. Vous perdez cela lorsque vous ajoutez vos clés multiples dans un boîtier différent, car comment les récupérez-vous? Essayez toutes les combinaisons majuscules / minuscules? Peu importe comment vous le faites, les performances ne seront pas celles d'une recherche normale de dictionnaire unique. Cela s'ajoute à d'autres lacunes plus évidentes comme la limitation des valeurs par clé, en fonction du nombre de caractères pouvant être convertis dans la clé.
Eugene Beresovsky
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.