Acesso sem distinção entre maiúsculas e minúsculas para dicionário genérico

244

Eu tenho um aplicativo que usa dlls gerenciadas. Uma dessas DLLs retorna um dicionário genérico:

Dictionary<string, int> MyDictionary;  

O dicionário contém teclas com maiúsculas e minúsculas.

Por outro lado, estou recebendo uma lista de chaves em potencial (string), mas não posso garantir o caso. Estou tentando obter o valor no dicionário usando as teclas. Mas é claro que o seguinte falhará, pois tenho uma incompatibilidade de casos:

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

Eu esperava que o TryGetValue tivesse um sinalizador de maiúsculas e minúsculas como mencionado no documento do MSDN , mas parece que isso não é válido para dicionários genéricos.

Existe uma maneira de obter o valor desse dicionário ignorando o caso principal? Existe uma solução melhor do que criar uma nova cópia do dicionário com o parâmetro StringComparer.OrdinalIgnoreCase apropriado ?

TocToc
fonte

Respostas:

514

Não há como especificar a StringComparerno ponto em que você tenta obter um valor. Se você pensar sobre isso "foo".GetHashCode()e "FOO".GetHashCode()for totalmente diferente, não haverá uma maneira razoável de implementar uma obtenção que não diferencia maiúsculas de minúsculas em um mapa de hash que diferencia maiúsculas de minúsculas.

No entanto, você pode criar um dicionário que não diferencia maiúsculas de minúsculas usando: -

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

Ou crie um novo dicionário que não diferencia maiúsculas de minúsculas com o conteúdo de um dicionário que diferencia maiúsculas de minúsculas existente (se tiver certeza de que não há colisão de maiúsculas e minúsculas): -

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

Este novo dicionário, em seguida, usa a GetHashCode()implementação no StringComparer.OrdinalIgnoreCasemodo comparer.GetHashCode("foo")e comparer.GetHashcode("FOO")dar-lhe o mesmo valor.

Como alternativa, se houver apenas alguns elementos no dicionário e / ou você precisar pesquisar apenas uma ou duas vezes, poderá tratar o dicionário original como um IEnumerable<KeyValuePair<TKey, TValue>>e apenas iterá-lo: -

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

Ou, se preferir, sem o 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;
  }
}

Isso economiza o custo de criação de uma nova estrutura de dados, mas, em troca, o custo de uma pesquisa é O (n) em vez de O (1).

Iain Galloway
fonte
Na verdade, faz sentido. Muito obrigado pela explicação.
TocToc 5/11/12
1
Não há razão para manter o dicionário antigo por perto e instanciar o novo, pois qualquer colisão de casos fará com que ele exploda. Se você sabe que não terá colisões, poderá usar maiúsculas e minúsculas desde o início.
Rhys Bevilaqua
2
Faz dez anos que uso o .NET e agora descobri isso! Por que você usa Ordinal em vez de CurrentCulture?
Jordânia
Bem, isso depende do comportamento que você deseja. Se o usuário estiver fornecendo a chave por meio da interface do usuário (ou se você precisar considerar, por exemplo, ss e ß iguais), precisará usar uma cultura diferente, mas, considerando que o valor está sendo usado como a chave para um hashmap proveniente de uma dependência externa, acho que 'OrdinalCulture' é uma suposição razoável.
Iain Galloway
1
default(KeyValuePair<T, U>)não é null- é um KeyValuePaironde Key=default(T)e Value=default(U). Portanto, você não pode usar o ?.operador no exemplo LINQ; você precisará pegar FirstOrDefault()e, em seguida (neste caso específico), verificar se Key == null.
Asherber
38

Para você, LINQers por aí que nunca usam um construtor de dicionário comum:

myCollection.ToDictionary(x => x.PartNumber, x => x.PartDescription, StringComparer.OrdinalIgnoreCase)
Derpy
fonte
8

Não é muito elegante, mas caso você não possa alterar a criação do dicionário, e tudo o que você precisa é de um truque sujo, e quanto a isso:

var item = MyDictionary.Where(x => x.Key.ToLower() == MyIndex.ToLower()).FirstOrDefault();
    if (item != null)
    {
        TheValue = item.Value;
    }
Shoham
fonte
13
ou apenas isso: new Dictionary <string, int> (otherDict, StringComparer.CurrentCultureIgnoreCase);
Jordânia
6
Conforme "Práticas recomendadas para usar seqüências de caracteres no .NET Framework", use em ToUpperInvariantvez de ToLower. msdn.microsoft.com/en-us/library/dd465121%28v=vs.110%29.aspx
Fred
Isso foi bom para mim, onde tive que verificar as chaves retrospectivamente de maneira insensível. I racionalizado um pouco maisvar item = MyDictionary.FirstOrDefault(x => x.Key.ToUpperInvariant() == keyValueToCheck.ToUpperInvariant());
Jay
Por que não apenas dict.Keys.Contains("bla", appropriate comparer)? Além disso, você não obterá nulo para FirstOrDefault, pois keyvaluepair em C # é uma estrutura.
Nawfal