Cuando trabajamos con diccionarios en .NET, es muy común comprobar si existe un elemento con una clave determinada antes de obtenerlo para evitar una excepción KeyNotFoundException
:
var numbers = new Dictionary<int, string>()
{
[1] = "One",
[2] = "Two",
[3] = "Three"
};
// Aseguramos que el elemento existe antes de obtenerlo
if (numbers.ContainsKey(3))
{
Console.WriteLine(numbers[3]);
}
Sin embargo, esta comprobación tiene coste. Es decir, aunque el acceso a los diccionarios sea muy rápido -O(1)-, no quiere decir que no consuma tiempo, por lo que es interesante conocer alternativas más eficientes para estos escenarios.
Cómo evitar la doble búsqueda
.NET nos ofrece métodos para evitar la comprobación previa de la existencia de un elemento en un diccionario en los casos de uso más frecuentes. Por ejemplo, TryGetValue()
permite, en una única llamada, comprobar la existencia de un elemento en un diccionario y obtener su valor. Un ejemplo de uso es el siguiente:
// Con comprobación previa:
if (numbers.ContainsKey(3))
{
Console.WriteLine(numbers[3]);
}
// Sin comprobación previa:
if (numbers.TryGetValue(3, out value))
{
Console.WriteLine(value);
}
Como podéis intuir del ejemplo, TryGetValue()
recibe como argumento la clave del elemento que queremos obtener y devuelve un bool
que indica si el elemento existe o no. Además, si el elemento existe, devuelve el valor asociado a la clave en el parámetro de salida out
, que será establecido a null
en caso contrario.
¿Y cómo afecta esto al rendimiento? Pues para verlo podemos tirar de nuestro amigo BenchmarkDotNet, donde creamos una prueba sencilla como la siguiente:
[MemoryDiagnoser]
public class DictionaryBenchmarks
{
[Params(1, 4)] // Un elemento existe y el otro no
public int Index { get; set; }
private readonly Dictionary<int, string> _numbers = new()
{
[1] = "One",
[2] = "Two",
[3] = "Three"
};
[Benchmark]
public string? GetWithContainsKey()
{
return _numbers.ContainsKey(Index) ? _numbers[Index] : null;
}
[Benchmark]
public string? GetWithTryGetValue()
{
return _numbers.TryGetValue(Index, out var contains) ? contains : null;
}
}
La siguiente tabla muestra los resultados cuando intentamos obtener elementos que existen en el diccionario. Como se puede observar, el método TryGetValue()
es más del doble de rápido que usar ContainsKey()
y luego obtener el valor:
| Method | Index | Mean | Error | StdDev | Allocated |
|------------------- |------ |---------:|----------:|----------:|----------:|
| GetWithContainsKey | 1 | 7.612 ns | 0.1634 ns | 0.1528 ns | - |
| GetWithTryGetValue | 1 | 3.044 ns | 0.0315 ns | 0.0263 ns | - |
En cambio, si el elemento no existe en el diccionario, prácticamente se comportarán igual, puesto que en el caso de usar ContainsKey()
no será necesario realizar una llamada posterior para obtenerlo:
| Method | Index | Mean | Error | StdDev | Allocated |
|--------------------- |------ |---------:|----------:|----------:|----------:|
| GetWithContainsKey | 4 | 3.496 ns | 0.0275 ns | 0.0230 ns | - |
| GetkWithTryGetValue | 4 | 3.528 ns | 0.0196 ns | 0.0183 ns | - |
En resumen, usar TryGetValue()
puede mejorar el rendimiento si a posteriori vamos a obtener el elemento. De hecho, es la recomendación oficial que podemos ver en forma de aviso CA1854 cuando compilamos un proyecto que use ContainsKey()
seguido de un acceso al elemento:
Aún no hay comentarios, ¡sé el primero!
Enviar un nuevo comentario