Accès insensible à la casse pour le dictionnaire générique

244

J'ai une application qui utilise des DLL gérées. Une de ces DLL renvoie un dictionnaire générique:

Dictionary<string, int> MyDictionary;  

Le dictionnaire contient des touches avec majuscules et minuscules.

D'un autre côté, je reçois une liste de clés potentielles (chaîne) mais je ne peux pas garantir le cas. J'essaye d'obtenir la valeur dans le dictionnaire en utilisant les clés. Mais bien sûr, ce qui suit échouera car j'ai une incompatibilité de cas:

bool Success = MyDictionary.TryGetValue( MyIndex, out TheValue );  

J'espérais que TryGetValue aurait un indicateur de casse ignoré comme mentionné dans la doc MSDN , mais il semble que ce ne soit pas valide pour les dictionnaires génériques.

Existe-t-il un moyen d'obtenir la valeur de ce dictionnaire en ignorant le cas clé? Existe-t-il une meilleure solution de contournement que la création d'une nouvelle copie du dictionnaire avec le paramètre StringComparer.OrdinalIgnoreCase approprié ?

Toc Toc
la source

Réponses:

515

Il n'y a aucun moyen de spécifier un StringComparerau point où vous essayez d'obtenir une valeur. Si vous y pensez "foo".GetHashCode()et "FOO".GetHashCode()êtes totalement différent, il n'y a donc aucun moyen raisonnable d'implémenter un get insensible à la casse sur une carte de hachage sensible à la casse.

Vous pouvez, cependant, créer un dictionnaire insensible à la casse en premier lieu en utilisant: -

var comparer = StringComparer.OrdinalIgnoreCase;
var caseInsensitiveDictionary = new Dictionary<string, int>(comparer);

Ou créez un nouveau dictionnaire ne respectant pas la casse avec le contenu d'un dictionnaire sensible à la casse existant (si vous êtes sûr qu'il n'y a pas de collision de cas): -

var oldDictionary = ...;
var comparer = StringComparer.OrdinalIgnoreCase;
var newDictionary = new Dictionary<string, int>(oldDictionary, comparer);

Ce nouveau dictionnaire utilise alors l' GetHashCode()implémentation StringComparer.OrdinalIgnoreCaseainsi comparer.GetHashCode("foo")et comparer.GetHashcode("FOO")vous donne la même valeur.

Alternativement, s'il n'y a que quelques éléments dans le dictionnaire, et / ou que vous n'avez besoin de chercher qu'une ou deux fois, vous pouvez traiter le dictionnaire d'origine comme un IEnumerable<KeyValuePair<TKey, TValue>>et simplement le parcourir: -

var myKey = ...;
var myDictionary = ...;
var comparer = StringComparer.OrdinalIgnoreCase;
var value = myDictionary.FirstOrDefault(x => String.Equals(x.Key, myKey, comparer)).Value;

Ou si vous préférez, sans LINQ: -

var myKey = ...;
var myDictionary = ...;
var comparer = StringComparer.OrdinalIgnoreCase;
int? value;
foreach (var element in myDictionary)
{
  if (String.Equals(element.Key, myKey, comparer))
  {
    value = element.Value;
    break;
  }
}

Cela vous évite le coût de création d'une nouvelle structure de données, mais en retour, le coût d'une recherche est O (n) au lieu de O (1).

Iain Galloway
la source
En effet, cela a du sens. Merci beaucoup pour l'explication.
TocToc
1
Il n'y a aucune raison de conserver l'ancien dictionnaire et d'instancier le nouveau car toute collision de cas le ferait exploser. Si vous savez que vous n'obtiendrez pas de collisions, vous pouvez tout aussi bien utiliser la casse dès le départ.
Rhys Bevilaqua
2
Cela fait dix ans que j'utilise .NET et je viens de comprendre cela !! Pourquoi utilisez-vous Ordinal au lieu de CurrentCulture?
Jordan
Eh bien, cela dépend du comportement que vous souhaitez. Si l'utilisateur fournit la clé via l'interface utilisateur (ou si vous devez considérer par exemple ss et ß égal), vous devrez utiliser une culture différente, mais étant donné que la valeur est utilisée comme clé pour une table de hachage provenant de une dépendance externe, je pense que «OrdinalCulture» est une hypothèse raisonnable.
Iain Galloway
1
default(KeyValuePair<T, U>)n'est pas null- c'est un KeyValuePairKey=default(T)et Value=default(U). Vous ne pouvez donc pas utiliser l' ?.opérateur dans l'exemple LINQ; vous devrez saisir FirstOrDefault()puis (pour ce cas particulier) vérifier si Key == null.
asherber
38

Pour vous LINQers là-bas qui n'utilisez jamais un constructeur de dictionnaire ordinaire:

myCollection.ToDictionary(x => x.PartNumber, x => x.PartDescription, StringComparer.OrdinalIgnoreCase)
Ridicule
la source
8

Ce n'est pas très élégant mais au cas où vous ne pouvez pas changer la création du dictionnaire, et tout ce dont vous avez besoin est un hack sale, que diriez-vous:

var item = MyDictionary.Where(x => x.Key.ToLower() == MyIndex.ToLower()).FirstOrDefault();
    if (item != null)
    {
        TheValue = item.Value;
    }
Shoham
la source
13
ou juste ceci: nouveau Dictionary <string, int> (otherDict, StringComparer.CurrentCultureIgnoreCase);
Jordan
6
Selon «Meilleures pratiques pour l'utilisation de chaînes dans le .NET Framework», utilisez ToUpperInvariantau lieu de ToLower. msdn.microsoft.com/en-us/library/dd465121%28v=vs.110%29.aspx
Fred
C'était bon pour moi, où j'ai dû vérifier rétrospectivement les clés de manière insensible. Je l'ai rationalisé un peu plusvar item = MyDictionary.FirstOrDefault(x => x.Key.ToUpperInvariant() == keyValueToCheck.ToUpperInvariant());
Jay
Pourquoi pas juste dict.Keys.Contains("bla", appropriate comparer)? De plus, vous n'obtiendrez pas null pour FirstOrDefault car keyvaluepair en C # est une structure.
nawfal