129 votes

Obtenir plusieurs clés de la valeur spécifiée d'un dictionnaire générique ?

Il est facile d'obtenir la valeur d'une clé à partir d'un dictionnaire générique .NET :

Dictionary<int, string> greek = new Dictionary<int, string>();
greek.Add(1, "Alpha");
greek.Add(2, "Beta");
string secondGreek = greek[2];  // Beta

Mais essayer d'obtenir les clés d'une valeur n'est pas aussi simple, car il peut y avoir plusieurs clés :

int[] betaKeys = greek.WhatDoIPutHere("Beta");  // expecting single 2

1 votes

Pourquoi le type de retour int[] alors que vous attendez une seule valeur ?

3 votes

@Anar, lisez ma réponse à Domenic ; "Les valeurs en double sont peu probables mais pas impossibles".

0 votes

la clé d'une valeur ? Je pense que vous voulez dire les clés

154voto

Jon Skeet Points 692016

Ok, voici la version bidirectionnelle multiple :

using System;
using System.Collections.Generic;
using System.Text;

class BiDictionary<TFirst, TSecond>
{
    IDictionary<TFirst, IList<TSecond>> firstToSecond = new Dictionary<TFirst, IList<TSecond>>();
    IDictionary<TSecond, IList<TFirst>> secondToFirst = new Dictionary<TSecond, IList<TFirst>>();

    private static IList<TFirst> EmptyFirstList = new TFirst[0];
    private static IList<TSecond> EmptySecondList = new TSecond[0];

    public void Add(TFirst first, TSecond second)
    {
        IList<TFirst> firsts;
        IList<TSecond> seconds;
        if (!firstToSecond.TryGetValue(first, out seconds))
        {
            seconds = new List<TSecond>();
            firstToSecond[first] = seconds;
        }
        if (!secondToFirst.TryGetValue(second, out firsts))
        {
            firsts = new List<TFirst>();
            secondToFirst[second] = firsts;
        }
        seconds.Add(second);
        firsts.Add(first);
    }

    // Note potential ambiguity using indexers (e.g. mapping from int to int)
    // Hence the methods as well...
    public IList<TSecond> this[TFirst first]
    {
        get { return GetByFirst(first); }
    }

    public IList<TFirst> this[TSecond second]
    {
        get { return GetBySecond(second); }
    }

    public IList<TSecond> GetByFirst(TFirst first)
    {
        IList<TSecond> list;
        if (!firstToSecond.TryGetValue(first, out list))
        {
            return EmptySecondList;
        }
        return new List<TSecond>(list); // Create a copy for sanity
    }

    public IList<TFirst> GetBySecond(TSecond second)
    {
        IList<TFirst> list;
        if (!secondToFirst.TryGetValue(second, out list))
        {
            return EmptyFirstList;
        }
        return new List<TFirst>(list); // Create a copy for sanity
    }
}

class Test
{
    static void Main()
    {
        BiDictionary<int, string> greek = new BiDictionary<int, string>();
        greek.Add(1, "Alpha");
        greek.Add(2, "Beta");
        greek.Add(5, "Beta");
        ShowEntries(greek, "Alpha");
        ShowEntries(greek, "Beta");
        ShowEntries(greek, "Gamma");
    }

    static void ShowEntries(BiDictionary<int, string> dict, string key)
    {
        IList<int> values = dict[key];
        StringBuilder builder = new StringBuilder();
        foreach (int value in values)
        {
            if (builder.Length != 0)
            {
                builder.Append(", ");
            }
            builder.Append(value);
        }
        Console.WriteLine("{0}: [{1}]", key, builder);
    }
}

2 votes

D'après ce que j'ai lu dans msdn, cela ne devrait-il pas être un BiLookup au lieu d'un BiDictionary ? Non pas que ce soit important ou quoi que ce soit, je suis juste curieux de savoir si j'ai bien compris les choses ici...

0 votes

De même, si j'utilise GetByFirst et que je récupère la liste EmptySecondList, que j'y ajoute des éléments et que j'appelle à nouveau GetByFirst, ne vais-je pas obtenir une liste contenant des éléments et non une liste vide ?

0 votes

@Svish : Non, parce que lorsque vous essayez d'ajouter à la liste, une exception est levée (vous ne pouvez pas ajouter à un tableau). Et oui, BiLookup serait probablement un meilleur nom.

76voto

Jon Skeet Points 692016

Comme tout le monde l'a dit, il n'y a pas de correspondance entre la valeur et la clé dans un dictionnaire.

Je viens de remarquer que vous vouliez faire correspondre une valeur à plusieurs clés - je laisse cette solution ici pour la version à valeur unique, mais j'ajouterai ensuite une autre réponse pour une carte bidirectionnelle à entrées multiples.

L'approche normale à adopter ici est d'avoir deux dictionnaires - l'un cartographiant dans un sens et l'autre dans l'autre. Encapsulez-les dans une classe séparée, et déterminez ce que vous voulez faire lorsque vous avez une clé ou une valeur en double (par exemple, lancer une exception, écraser l'entrée existante, ou ignorer la nouvelle entrée). Personnellement, j'opterais probablement pour le lancement d'une exception - cela rend le comportement de réussite plus facile à définir. Quelque chose comme ceci :

using System;
using System.Collections.Generic;

class BiDictionary<TFirst, TSecond>
{
    IDictionary<TFirst, TSecond> firstToSecond = new Dictionary<TFirst, TSecond>();
    IDictionary<TSecond, TFirst> secondToFirst = new Dictionary<TSecond, TFirst>();

    public void Add(TFirst first, TSecond second)
    {
        if (firstToSecond.ContainsKey(first) ||
            secondToFirst.ContainsKey(second))
        {
            throw new ArgumentException("Duplicate first or second");
        }
        firstToSecond.Add(first, second);
        secondToFirst.Add(second, first);
    }

    public bool TryGetByFirst(TFirst first, out TSecond second)
    {
        return firstToSecond.TryGetValue(first, out second);
    }

    public bool TryGetBySecond(TSecond second, out TFirst first)
    {
        return secondToFirst.TryGetValue(second, out first);
    }
}

class Test
{
    static void Main()
    {
        BiDictionary<int, string> greek = new BiDictionary<int, string>();
        greek.Add(1, "Alpha");
        greek.Add(2, "Beta");
        int x;
        greek.TryGetBySecond("Beta", out x);
        Console.WriteLine(x);
    }
}

0 votes

J'aimerais qu'elle soit héritée d'une des collections de base, afin qu'elle puisse bénéficier de certaines des méthodes intégrées de cette collection. Des choses comme le support de linq et IEnumerable, etc.

2 votes

Je ne pense pas qu'il y ait de raison de la faire dériver d'une classe concrète - je n'aime pas l'héritage sans une réflexion approfondie - mais elle pourrait certainement implémenter IEnumerable, etc. En fait, elle pourrait implémenter IDictionary<TFirst, TSecond> et IDictionary<TSecond, TFirst>.

1 votes

(Bien que cela serait assez bizarre si TFirst et TSecond étaient les mêmes...)

27voto

Domenic Points 40761

Les dictionnaires ne sont pas vraiment conçus pour fonctionner de cette manière, car si l'unicité des clés est garantie, celle des valeurs ne l'est pas. Donc, par exemple, si vous avez

var greek = new Dictionary<int, string> { { 1, "Alpha" }, { 2, "Alpha" } };

Qu'espérez-vous obtenir pour greek.WhatDoIPutHere("Alpha") ?

Il ne faut donc pas s'attendre à ce que quelque chose comme ça soit intégré au cadre. Vous auriez besoin de votre propre méthode pour vos propres utilisations uniques - voulez-vous retourner un tableau (ou IEnumerable<T> ) ? Voulez-vous lancer une exception s'il y a plusieurs clés avec la valeur donnée ? Et s'il n'y en a pas ?

Personnellement, j'opterais pour un énumérable, comme ceci :

IEnumerable<TKey> KeysFromValue<TKey, TValue>(this Dictionary<TKey, TValue> dict, TValue val)
{
    if (dict == null)
    {
        throw new ArgumentNullException("dict");
    }
    return dict.Keys.Where(k => dict[k] == val);
}

var keys = greek.KeysFromValue("Beta");
int exceptionIfNotExactlyOne = greek.KeysFromValue("Beta").Single();

0 votes

Une solution élégante, mais qui doit fonctionner en 2.0. Les valeurs dupliquées sont peu probables mais pas impossibles, retourner une collection serait mieux.

23voto

CMS Points 315406

Peut-être que la façon la plus simple de le faire, sans Linq, peut être de boucler sur les paires :

int betaKey; 
foreach (KeyValuePair<int, string> pair in lookup)
{
    if (pair.Value == value)
    {
        betaKey = pair.Key; // Found
        break;
    }
}
betaKey = -1; // Not found

Si vous aviez Linq, vous auriez pu le faire facilement de cette façon :

int betaKey = greek.SingleOrDefault(x => x.Value == "Beta").Key;

0 votes

Dour, mais vous avez un type var au-dessus ?! vous êtes sûrement en 3.0 ? voyez aussi ma mise à jour ci-dessous.

0 votes

Mes excuses, j'ai utilisé "var" simplement pour réduire la saisie. Je préférerais ne pas faire une recherche linéaire, le dictionnaire pourrait être grand.

2 votes

var est une caractéristique de la langue et non du cadre. Vous pouvez utiliser null-coalescing de C#-6.0 et toujours cibler CF-2.0 si tu le veux vraiment.

2voto

dbkk Points 5305

La classe Dictionary n'est pas optimisée pour ce cas, mais si vous vouliez vraiment le faire (en C# 2.0), vous pouvez le faire :

public List<TKey> GetKeysFromValue<TKey, TVal>(Dictionary<TKey, TVal> dict, TVal val)
{
   List<TKey> ks = new List<TKey>();
   foreach(TKey k in dict.Keys)
   {
      if (dict[k] == val) { ks.Add(k); }
   }
   return ks;
}

Je préfère la solution LINQ pour l'élégance, mais c'est la méthode 2.0.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X