Acceso sin distinción entre mayúsculas y minúsculas para diccionario genérico

244

Tengo una aplicación que usa dlls administrados. Uno de esos dlls devuelve un diccionario genérico:

Dictionary<string, int> MyDictionary;  

El diccionario contiene claves con mayúsculas y minúsculas.

Por otro lado, obtengo una lista de posibles claves (cadenas), sin embargo, no puedo garantizar el caso. Estoy tratando de obtener el valor en el diccionario usando las teclas. Pero, por supuesto, lo siguiente fallará ya que tengo un caso que no coincide:

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

Esperaba que TryGetValue tuviera un indicador de caso de ignorar como se menciona en el documento de MSDN , pero parece que esto no es válido para diccionarios genéricos.

¿Hay alguna manera de obtener el valor de ese diccionario ignorando el caso clave? ¿Existe una solución alternativa mejor que crear una nueva copia del diccionario con el parámetro StringComparer.OrdinalIgnoreCase adecuado ?

TOC Toc
fuente

Respuestas:

514

No hay forma de especificar un StringCompareren el punto donde intenta obtener un valor. Si lo piensa, "foo".GetHashCode()y "FOO".GetHashCode()es totalmente diferente, por lo que no hay una forma razonable de implementar un get que no distinga entre mayúsculas y minúsculas en un mapa hash sensible a mayúsculas y minúsculas.

Sin embargo, puede crear un diccionario que no distinga entre mayúsculas y minúsculas en primer lugar usando:

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

O cree un nuevo diccionario que no distinga mayúsculas de minúsculas con el contenido de un diccionario existente que distinga mayúsculas de minúsculas (si está seguro de que no hay colisiones de mayúsculas y minúsculas):

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

Este nuevo diccionario luego usa la GetHashCode()implementación enStringComparer.OrdinalIgnoreCase so comparer.GetHashCode("foo")y le comparer.GetHashcode("FOO")da el mismo valor.

Alternativamente, si solo hay unos pocos elementos en el diccionario, y / o solo necesita buscar una o dos veces, puede tratar el diccionario original como un IEnumerable<KeyValuePair<TKey, TValue>> y simplemente iterar sobre él: -

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

O si lo prefiere, sin el 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;
  }
}

Esto le ahorra el costo de crear una nueva estructura de datos, pero a cambio el costo de una búsqueda es O (n) en lugar de O (1).

Iain Galloway
fuente
De hecho tiene sentido. Muchas gracias por la explicación.
TocToc
1
No hay ninguna razón para mantener el antiguo diccionario y crear una instancia nueva, ya que cualquier colisión de casos hará que explote. Si sabe que no tendrá colisiones, puede utilizar mayúsculas y minúsculas desde el principio.
Rhys Bevilaqua
2
¡Han pasado diez años que he estado usando .NET y ahora acabo de resolver esto! ¿Por qué usas Ordinal en lugar de CurrentCulture?
Jordan
Bueno, depende del comportamiento que quieras. Si el usuario proporciona la clave a través de la interfaz de usuario (o si necesita considerar, por ejemplo, ss y ß igual), entonces deberá usar una cultura diferente, pero dado que el valor se está utilizando como la clave para un hashmap proveniente de una dependencia externa, creo que 'OrdinalCulture' es una suposición razonable.
Iain Galloway
1
default(KeyValuePair<T, U>)no es null, es un KeyValuePairdónde Key=default(T)y Value=default(U). Por lo tanto, no puede usar el ?.operador en el ejemplo de LINQ; tendrá que tomar FirstOrDefault()y luego (para este caso en particular) verificar para ver si Key == null.
asherber
38

Para ustedes LINQers que nunca usan un constructor de diccionario regular:

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

No es muy elegante, pero en caso de que no pueda cambiar la creación del diccionario, y todo lo que necesita es un truco sucio, ¿qué tal esto:

var item = MyDictionary.Where(x => x.Key.ToLower() == MyIndex.ToLower()).FirstOrDefault();
    if (item != null)
    {
        TheValue = item.Value;
    }
Shoham
fuente
13
o simplemente esto: nuevo Diccionario <string, int> (otherDict, StringComparer.CurrentCultureIgnoreCase);
Jordan
66
Según las "Mejores prácticas para usar cadenas en el .NET Framework", use en ToUpperInvariantlugar de ToLower. msdn.microsoft.com/en-us/library/dd465121%28v=vs.110%29.aspx
Fred
Esto fue bueno para mí, donde tuve que revisar retrospectivamente las claves de una manera insensible. Lo simplifiqué un poco másvar item = MyDictionary.FirstOrDefault(x => x.Key.ToUpperInvariant() == keyValueToCheck.ToUpperInvariant());
Jay
¿Por qué no solo dict.Keys.Contains("bla", appropriate comparer)? Además, no será nulo para FirstOrDefault ya que keyvaluepair en C # es una estructura.
nawfal