web-dev-qa-db-fra.com

Dictionnaire .NET avec deux clés et une valeur

Existe-t-il un dictionnaire disponible dans .NET pouvant contenir 2 clés et une valeur .

Dictionary(Of TKey, Of TKey, TValue)

J'ai besoin de stocker deux clés et, à certains moments, de rechercher un élément à l'aide de la touche 1 et d'autres fois, à l'aide de la touche 2.

Ma solution actuelle est de maintenir deux dictionnaires

Dictionary<string, long> Dict1 = new Dictionary<string, long>();
Dictionary<long, long> Dict2 = new Dictionary<long, long>();

et quand il faudra ajouter un article, je l’ajouterai aux deux dictionnaires.

Dict1.Add("abc", 111);
Dict2.Add(345, 111);

et ensuite, je rechercherai un élément de l'un ou l'autre de ces dictionnaires en fonction de la clé à rechercher.

Même chose que je ferai lors de la suppression ou de la mise à jour d'un élément.

J'ai pensé à la clé composite, mais je ne sais pas comment la configurer et je ne veux pas perdre de vitesse dans la recherche de l'élément.

Existe-t-il une solution disponible dans .NET pour avoir un dictionnaire pouvant contenir plusieurs clés?

34
Nuts

Si vous souhaitez que votre valeur soit "trouvable" à partir de l'une ou l'autre clé, je voudrais simplement utiliser deux dictionnaires, comme vous le faites maintenant. Cependant, je voudrais envelopper ceci dans une classe , avec des noms de méthodes comme FindByXXX et FindByYYY

La question beaucoup plus difficile est de savoir comment faire une suppression , car vous devez connaître les deux clés au moment de la suppression. Peut-être que votre valeur stocke les deux clés afin que vous puissiez la transmettre à votre méthode de suppression. Peut-être n’avez-vous jamais besoin de supprimer des éléments des dictionnaires. Ou le code qui doit supprimer des éléments connaît les deux clés.

Il n’existe donc pas de dictionnaire standard pour le faire, car les exigences diffèrent d’un utilisateur à l’autre.

(Notez que vous ne voulez pas de dictionnaire avec une clé composite, cela vous obligerait à connaître les deux clés chaque fois que vous souhaitiez rechercher un élément.)

13
Ian Ringrose

Peut-être, quelque chose comme ça:

public class TwoKeyDictionary<Tkey1, Tkey2, TValue>
{
    private object m_data_lock = new object();
    private Dictionary<Tkey1, Tkey2> m_dic1 = new Dictionary<Tkey1, Tkey2>();
    private Dictionary<Tkey2, TValue> m_dic2 = new Dictionary<Tkey2, TValue>();

    public void AddValue(Tkey1 key1, Tkey2 key2, TValue value)
    {
        lock(m_data_lock)
        {
            m_dic1[key1] = key2;
            m_dic2[key2] = value;
        }
    }

    public TValue getByKey1(Tkey1 key1)
    {
        lock(m_data_lock)
            return m_dic2[m_dic1[key1]];
    }

    public TValue getByKey2(Tkey key2)
    {
        lock(m_data_lock)
            return m_dic2[key2];
    }

    public void removeByKey1(Tkey1 key1)
    {
        lock(m_data_lock)
        {
            Tkey2 tmp_key2 =   m_dic1[key1];
            m_dic1.Remove(key1);
            m_dic2.Remove(tmp_key2);
        }
    }

    public void removeByKey2(Tkey2 key2)
    {
        lock(m_data_lock)
        {
            Tkey1 tmp_key1 = m_dic1.First((kvp) => kvp.Value.Equals(key2)).Key;
            m_dic1.Remove(tmp_key1);
            m_dic2.Remove(key2);
        }
    }
}

Je peux proposer une deuxième solution, mais elle semble plus lente et plus moche que la première.

public class TwoKeysDictionary<K1, K2, V>
{
    private class TwoKeysValue<K1, K2, V>
    {
        public K1 Key1 { get; set; }
        public K2 Key2 { get; set; }
        public V Value { get; set; }
    }

    private List<TwoKeysValue<K1, K2, V>> m_list = new List<TwoKeysValue<K1, K2, V>>();

    public void Add(K1 key1, K2 key2, V value)
    {
        lock (m_list)
            m_list.Add(new TwoKeysValue<K1, K2, V>() { Key1 = key1, Key2 = key2, Value = value });
    }

    public V getByKey1(K1 key1)
    {
        lock (m_list)
            return m_list.First((tkv) => tkv.Key1.Equals(key1)).Value;
    }

    public V getByKey2(K2 key2)
    {
        lock (m_list)
            return m_list.First((tkv) => tkv.Key2.Equals(key2)).Value;
    }

    public void removeByKey1(K1 key1)
    {
        lock (m_list)
            m_list.Remove(m_list.First((tkv) => tkv.Key1.Equals(key1)));
    }

    public void removeByKey2(K2 key2)
    {
        lock (m_list)
            m_list.Remove(m_list.First((tkv) => tkv.Key2.Equals(key2)));
    }
}

Dans de très mauvais cas, lorsque les clés sont de grandes structures (c.-à-d. Les types de grande valeur) et que les clés sont égales par taille, et les valeurs sont de petits types de valeur (par exemple, un octet), avec la première solution que vous aviez: un ensemble de clés1 , deux jeux de Key2, un jeu de valeurs = 3 jeux de gros objets et 1 jeu de petites valeurs . Avec la deuxième solution, vous aviez: un jeu de Key1, un jeu de Key2, un jeu de valeurs = 2 jeux de gros objets et petit ensemble avec des valeurs . Ie Si vous utilisez une première solution, vous avez besoin de 50% (ou moins) d’espace mémoire supplémentaire par rapport à la seconde, mais une seconde solution est très lente par rapport à la première.

10
Alexander Kiselev

Votre solution a un impact important sur l’empreinte mémoire de votre application. À mesure que le dictionnaire grandit, il faudra au moins le double de la quantité de mémoire (pour les types de valeur) requise pour stocker les données réelles.

Vous pourriez probablement aborder cela sous un angle différent. Avoir deux dictionnaires:

var lookupDictionary = new Dictionary<string, string>();
var valuesDictionary = new Dictionary<string, [YourValueType]>();

A partir de là dans sa assez simple.

// Add a new entry into the values dictionary and give it a unique key
valuesDictionary.Add("FooBar", "FUBAR VALUE");

// Add any number of lookup keys with the same value key
lookupDictionary.Add("Foo", "FooBar");
lookupDictionary.Add("Bar", "FooBar");
lookupDictionary.Add("Rab", "FooBar");
lookupDictionary.Add("Oof", "FooBar");

Lorsque vous avez besoin de trouver quelque chose dans valuesDictionary, vous appuyez d'abord sur lookupDictionary. Cela vous donnera la clé de la valeur que vous recherchez dans la variable valuesDictionary.

MODIFIER

Je n'ai pas abordé la question de la suppression dans ma réponse, alors voici: D

Vous devez cliquer sur lookupDictionary pour trouver la clé de valeur, puis supprimer toutes les entrées de lookupDictionary qui ont cette valeur .

Devrait être assez simple et sûr car la valuesDictionary a la garantie de posséder une clé unique. Par conséquent, vous _ ne supprimerez pas accidentellement une clé de recherche d'une autre valeur.

Cependant, comme Ian Ringrose l’a souligné dans un commentaire, vous allez effectuer une analyse complète de la variable lookupDictionary à supprimer. Cela peut avoir un impact indésirable sur les performances dans les boucles serrées, etc.

Je ne peux pas vraiment penser à un bon moyen de résoudre ce problème pour le moment. Peut-être quelqu'un d'autre pourrait-il avoir des idées sur la manière d'améliorer cela.

J'espère que ça aide.

4
Alex

Si vous utilisez C # 7.0, la meilleure façon de le faire est d'utiliser des types et des littéraux de tuple:

// Declare
var dict = new Dictionary<(string, long), long>();

// Add
dict.Add(("abc", 345), 111);

// Get
var searchedValue = dict[("abc", 345)];
3
Hüseyin Yağlı

Vous ne pouvez pas le faire avec un seul dictionnaire sans perdre votre vitesse de recherche. La raison en est que si vous créez une clé composite, aucune valeur significative ne peut être renvoyée lorsque vous substituez GetHashCode. Cela signifie qu'une comparaison d'égalité doit être effectuée pour chaque clé jusqu'à ce qu'une entrée de dictionnaire soit trouvée. Dans ce cas, vous auriez également un problème potentiel avec une clé composite: puisque votre méthode Equals vérifierait si une propriété est identique, les clés suivantes sont essentiellement des clés en double {Id = 1, Name = "Bob"} Id = 1, Name = "Anna"}, ce qui ne me donne pas un sentiment de flou chaleureux.

Cela vous laisse avec un dictionnaire ou des dictionnaires avec votre propre classe.

2
Ananke

Ce n'est pas un dictionnaire approprié, mais peut être utilisé pour des fonctionnalités d'ajout/suppression de type dictionnaire.

Cela peut également être rendu générique, avec une implémentation appropriée d'IComparable dans les types de clés et une modification du code du dictionnaire en conséquence. (Remarque: les valeurs par défaut des clés ne sont pas autorisées pour gérer les ambiguïtés!)

internal class KeyValueSet //this dictionary item is tailor made for this example
{
    public string KeyStr { get; set; }
    public int KeyInt { get; set; }
    public int Value { get; set; }

    public KeyValueSet() { }

    public KeyValueSet(string keyStr, int keyInt, int value)
    {
        KeyStr = keyStr;
        KeyInt = keyInt;
        Value = value;
    }
}

public class DoubleKeyDictionary
{
    List<KeyValueSet> _list = new List<KeyValueSet>();

    private void Add(KeyValueSet set)
    {
        if (set == null)
            throw new InvalidOperationException("Cannot add null");
        if (string.IsNullOrEmpty(set.KeyStr) && set.KeyInt == 0)
            throw new InvalidOperationException("Invalid key");
        if (!string.IsNullOrEmpty(set.KeyStr) && _list.Any(l => l.KeyStr.Equals(set.KeyStr))
            || set.KeyInt != 0 && _list.Any(l => l.KeyInt == set.KeyInt))
            throw new InvalidOperationException("Either of keys exists");
        _list.Add(set);
    }

    public void Add(string keyStr, int keyInt, int value)
    {
        Add(new KeyValueSet { KeyInt = keyInt, KeyStr = keyStr, Value = value });
    }

    public void Add(string key, int value)
    {
        Add(new KeyValueSet { KeyInt = 0, KeyStr = key, Value = value });
    }

    public void Add(int key, int value)
    {
        Add(new KeyValueSet { KeyInt = key, KeyStr = string.Empty, Value = value });
    }

    public void Remove(int key)
    {
        if (key == 0)
            throw new InvalidDataException("Key not found");
        var val = _list.First(l => l.KeyInt == key);
        _list.Remove(val);
    }

    public void Remove(string key)
    {
        if (string.IsNullOrEmpty(key))
            throw new InvalidDataException("Key not found");
        var val = _list.First(l => l.KeyStr == key);
        _list.Remove(val);
    }

    public void Remove(KeyValueSet item)
    {
        _list.Remove(item);
    }

    public int this[int index]
    {
        get
        {
            if (index != 0 && _list.Any(l => l.KeyInt == index))
                return _list.First(l => l.KeyInt == index).Value;
            throw new InvalidDataException("Key not found");
        }
        set
        {
            Add(index, value);
        }
    }

    public int this[string key]
    {
        get
        {
            if (!string.IsNullOrEmpty(key) && _list.Any(l => l.KeyStr == key))
                return _list.First(l => l.KeyStr == key).Value;
            throw new InvalidDataException("Key not found");
        }
        set
        {
            Add(key, value);
        }
    }
}

Tester la DoubleKeyDictionary

var dict = new DoubleKeyDictionary();
dict.Add(123, 1);
dict.Add(234, 2);
dict.Add("k1", 3);
dict.Add("k2", 4);            
dict[456] = 5;
dict["k3"] = 6;
dict.Add("k4", 567, 7);
dict.Remove(123);

Console.WriteLine(dict[234]); //2
Console.WriteLine(dict["k2"]); //4
Console.WriteLine(dict[456]); //5
Console.WriteLine(dict[567]); //7
Console.WriteLine(dict["k4"]); //7
Console.WriteLine(dict[123]); //exception
1
Arghya C

question intéressante, voici une solution… .. Vous devez cependant ajouter un indexeur pour chaque type de clé que vous souhaitez prendre en charge.

public class NewDic<T>
{
    public void Add(string key1, long key2, T value)
    {
        mDic.Add(key1, value);
        mDic.Add(key2, value);
    }

    public T this[string s]
    {
        get { return mDic[s]; }
    }

    public T this[long l]
    {
        get { return mDic[l]; }
    }


    Dictionary<object, T> mDic = new Dictionary<object, T>();
}

        NewDic<long> dic = new NewDic<long>();

        dic.Add("abc", 20, 10);

        Console.WriteLine(dic["abc"]);
        Console.WriteLine(dic[20]);
1
Serve Laurijssen

Que diriez-vous d'un Dictionary<Tuple<string, long>, long>? Les n-uplets sont comparés par valeur, il devrait donc indexer uniquement de la manière attendue. De plus, vous n'aurez plus besoin de placer la valeur long à deux endroits (et de gérer la douleur de la synchronisation des valeurs partout).

Que diriez-vous de cette approche? Fondamentalement, utilisez toujours une stratégie basée sur un dictionnaire, mais placez-la dans une classe avec des propriétés d'indexeur surchargées. Donc, cela ressemble à un dictionnaire, ressemble à un dictionnaire, mais supporte plusieurs clés (pas comme un dictionnaire, LOL).

public class MultiKeyDictionary<TFirstKey, TSecondKey, TValue>
{
    private readonly Dictionary<TFirstKey, TValue> firstKeyDictionary = 
        new Dictionary<TFirstKey, TValue>();
    private readonly Dictionary<TSecondKey, TFirstKey> secondKeyDictionary = 
        new Dictionary<TSecondKey, TFirstKey>();

    public TValue this[TFirstKey idx]
    {
        get
        {
            return firstKeyDictionary[idx];
        }
        set
        {
            firstKeyDictionary[idx] = value;
        }
    }

    public TValue this[TSecondKey idx]
    {
        get
        {
            var firstKey = secondKeyDictionary[idx];
            return firstKeyDictionary[firstKey];
        }
        set
        {
            var firstKey = secondKeyDictionary[idx];
            firstKeyDictionary[firstKey] = value;
        }
    }

    public IEnumerable<KeyValuePair<TFirstKey, TValue>> GetKeyValuePairsOfFirstKey()
    {
        return firstKeyDictionary.ToList();
    }

    public IEnumerable<KeyValuePair<TSecondKey, TValue>> GetKeyValuePairsOfSecondKey()
    {
        var r = from s in secondKeyDictionary
            join f in firstKeyDictionary on s.Value equals f.Key
            select new KeyValuePair<TSecondKey, TValue>(s.Key, f.Value);

        return r.ToList();
    }

    public void Add(TFirstKey firstKey, TSecondKey secondKey, TValue value)
    {
        firstKeyDictionary.Add(firstKey, value);
        secondKeyDictionary.Add(secondKey, firstKey);
    }

    public bool Remove(TFirstKey firstKey)
    {
        if (!secondKeyDictionary.Any(f => f.Value.Equals(firstKey))) return false;

        var secondKeyToDelete = secondKeyDictionary.First(f => f.Value.Equals(firstKey));

        secondKeyDictionary.Remove(secondKeyToDelete.Key);
        firstKeyDictionary.Remove(firstKey);

        return true;
    }

    public bool Remove(TSecondKey secondKey)
    {
        if (!secondKeyDictionary.ContainsKey(secondKey)) return false;

        var firstKey = secondKeyDictionary[secondKey];
        secondKeyDictionary.Remove(secondKey);
        firstKeyDictionary.Remove(firstKey);

        return true;
    }
}

Testez le code ...

    static void Main(string[] args)
    {
        var dict = new MultiKeyDictionary<string, long, long>();
        dict.Add("abc", 111, 1234);
        dict.Add("def", 222, 7890);
        dict.Add("hij", 333, 9090);

        Console.WriteLine(dict["abc"]); // expect 1234
        Console.WriteLine(dict["def"]); // expect 7890
        Console.WriteLine(dict[333]); // expect 9090

        Console.WriteLine();
        Console.WriteLine("removing def");
        dict.Remove("def");

        Console.WriteLine();
        Console.WriteLine("now we have:");
        foreach (var d in dict.GetKeyValuePairsOfFirstKey())
        {
            Console.WriteLine($"{d.Key} : {d.Value}");
        }

        Console.WriteLine();
        Console.WriteLine("removing 333");
        dict.Remove(333);

        Console.WriteLine();
        Console.WriteLine("now we have:");
        foreach (var d in dict.GetKeyValuePairsOfSecondKey())
        {
            Console.WriteLine($"{d.Key} : {d.Value}");
        }

        Console.ReadLine();
    }
0
code4life

En tant que solution locale, j'utilise l'approche simple:

Imaginez que j'ai une collection de produits identifiés par une chaîne et un formulaire avec des boutons pour chacun des produits.

Lors de la gestion de l'état des boutons, je dois rechercher des boutons par clé de chaîne . Lors de la gestion des clics, je dois rechercher les ID de produit par instance de bouton.

Au lieu de conserver deux dictionnaires distincts, je procède comme suit:

public class SPurchaseOption
{
    public Button Button;
    public string ProductID;
    public string SomeOtherAssociatedData;
}

Dictionary<object, SPurchaseOption> purchaseOptions;

Lorsque les boutons sont initialisés, j’ajoute deux entrées dans la variable Dictionary, c.-à-d. 

Key: ProductID, Value: "SPurchaseOption"
Key: Button,    Value: "SPurchaseOption"

Pour une approche plus générale et si vous avez besoin d’un composant couramment utilisé, vous devrez créer un enveloppement autour de deux dictionnaires, à savoir:

public class DoubleKeyedDictionary<TKey1, TKey2, TValue>
{
    class SItem
    {
        public TKey1 key1;
        public TKey2 key2;
        public TValue value;
    }

    Dictionary<TKey1, SItem> dic1;
    Dictionary<TKey2, SItem> dic2;
}

cela donnera accès à la valeur et à la clé alternative par l'une des clés.

0
Zverev Evgeniy

Un moyen très grossier qui peut suffire jusqu'à ce que j'en trouve un meilleur.

class MyClass
{
  public string StringKey = "";
  public int IntKey = 0;

  public override Equals(object obj)
  {
    // Code to return true if all fields are equal
  }
}

Dictionary <MyClass, string> MyDict;
MyClass myClass;

MyDict[MyDict.Keys.FirstOrDefault(x => x.Equals(MyClass))];

Pour mon argent, la réponse disant à utiliser des tuples est la bonne. Malheureusement, mon NuGet est trop vieux pour obtenir le paquet ValueTuple que je voudrais utiliser, donc mes champs ne sont pas 'item1', 'item2' etc. Cela serait plus déroutant que ce que j'ai fait ici. Lorsque je change de version de VS/NuGet, c'est ValueTuples qui convient parfaitement à ce genre de situation. Deuxième fois cette semaine, j'ai rencontré le besoin!

0
B H

Au début, je pensais que je pouvais créer une classe qui implémentait IDictionary<TKey1, TValue> et IDictionary<TKey2, TValue> et n'avais qu'un dictionnaire unique comme champ et déléguait la plupart des méthodes au dictionnaire unique avec une logique minimale.

Le problème avec cette approche est que TKey1 et TKey2 pourraient être du même type, ce qui pose problème car cette nouvelle classe implémenterait deux fois la même interface. Quelle méthode le moteur d'exécution doit-il invoquer lorsque TKey1 est une string et que TKey2 est également une string?

Comme l'ont suggéré d'autres personnes ci-dessus, il est préférable de créer votre propre structure de données qui utilise un ou deux dictionnaires en arrière-plan. Par exemple, si vous saviez à l'avance que vous souhaitez utiliser une clé string et une int comme clés, vous pouvez utiliser cette approche:

public class StringIntDictionary<TValue> : IDictionary<string, TValue>, IDictionary<int, TValue>
{
    private IDictionary<object, TValue> _dictionary = new Dictionary<object, TValue>();
    // implement interface below, delegate to _dictionary
}

Cela vous permettrait d'utiliser les deux clés string et int:

var dict = StringIntDictionary<bool>();
dict["abc"] = true;
dict[123] = true;
0
armen.shimoon

Comme suggéré dans un commentaire de votre question, vous pouvez simplement utiliser une clé Object pour votre Dictionary:

Dictionary<Object, long> dict = new Dictionary<Object, long>();
dict.Add("abc", 111);
dict.Add(345, 111);

Pour obtenir une solution plus propre, vous pouvez insérer ce dictionnaire dans une classe personnalisée et créer votre version de la méthode Add:

public void Add(ISet<Object> keys, T value){
    foreach(Object k in keys)
    {
        _privateDict.Add(k, value); 
    }
}
0
davioooh