Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 28 de marzo de 2023
C#

A raíz del artículo publicado hace algunas semanas sobre las ventajas de usar diccionarios en lugar de listas, me llegaba vía comentarios un escenario en el que se utilizaba una clase List<T> para almacenar objetos a los que luego se accedía mediante clave. Lo diferencial del caso es que dichos objetos tenían varias claves únicas a través de las cuales podían ser localizados.

Por verlo por un ejemplo, el caso era más o menos como el que sigue:

public class FriendsCollection
{
    private List<Friend> _friends = new();
    ...
    public void Add(Friend friend)
    {
        _friends.Add(friend);
    }

    public Friend? GetById(int id) 
        => _friends.FirstOrDefault(f => f.Id == id);

    public Friend? GetByToken(string token) 
        => _friends.FirstOrDefault(f => f.Token == token);
}

Obviamente en este escenario no podemos sustituir alegremente la lista por un diccionario, porque necesitamos acceder a los elementos usando dos claves distintas. Pero, por supuesto, podemos conseguir también la ansiada búsqueda O(1) si le echamos muy poquito más de tiempo.

¿Podemos usar dos claves distintas para buscar en un diccionario?

Estrictamente hablando, la respuesta a esta pregunta es no. Un diccionario es al final una colección de pares clave-valor donde la clave tiene que ser única entre todos los elementos.

Pero podemos solucionarlo fácilmente ;) La fórmula más sencilla y fácil de entender consiste en utilizar en nuestra clase FriendsCollection dos diccionarios internos, uno para cada clave de búsqueda que queramos utilizar. Por tanto, la estructura de la clase podría ser como la siguiente:

public class FriendsCollection
{
    private readonly Dictionary<int, Friend> _byId = new();
    private readonly Dictionary<string, Friend> _byToken = new();
    ...
}

Por supuesto, la cosa no queda ahí. Ahora tendríamos que asegurar que al añadir elementos, ambos diccionarios son actualizados al mismo tiempo:

public void Add(Friend friend)
{
    _byId[friend.Id] = friend;
    _byToken[friend.Token] = friend;
}

Una pregunta que podríais haceros es este enfoque va a ocupar el doble de espacio en memoria por tener duplicada la colección de objetos Friend. Normalmente no, puesto que los diccionarios sólo mantendrán las claves y punteros hacia objetos Friend que estarán en memoria una única vez. Obviamente ocuparemos más memoria que manteniendo la colección en un simple List<T>, porque tendremos que almacenar dos conjuntos de claves distintas y las estructuras internas del diccionario, pero si el rendimiento es un aspecto importante, normalmente este sacrificio valdrá la pena.

Finalmente, ya podríamos reescribir nuestros métodos de consulta para retornar los valores, cada uno obtenido del diccionario interno correspondiente:

public Friend? GetById(int id) 
    => _byId.TryGetValue(id, out var found) ? found : null;
public Friend? GetByToken(string token) 
    => _byToken.TryGetValue(token, out var found) ? found : null;

¿Y no podría hacerlo con un único diccionario?

¡Pues claro! Si puedes asegurar que las claves que necesitas van a estar en espacios de valores diferentes, podrías conseguirlo con facilidad. La única diferencia es que en lugar de usar dos diccionarios utilizaríamos uno, en el que cada ítem estaría dos veces (una por cada clave).

Por ejemplo, si sabemos que tus objetos Friend usan como clave su propiedad numérica Id y una propiedad Token que sabemos que consta de X caracteres alfabéticos, podemos estar seguros de que no va a haber colisiones entre ambas. En este caso estamos seguros que los valores nunca coincidirán, así que podríamos hacer así:

public class FriendsCollection
{
    private readonly Dictionary<string, Friend> _friends = new();
    
    public void Add(Friend friend)
    {
        _friends[friend.Id.ToString()] = friend;
        _friends[friend.Token] = friend;
    }

    public Friend? GetById(int id) 
        => _friends.TryGetValue(id.ToString(), out var found) ? found : null;

    public Friend? GetByToken(string token) 
        => _friends.TryGetValue(token, out var found) ? found : null;

}    

Como habéis podido ver, usamos un único diccionario para almacenarlo todo. Rápido y eficaz 😉

Creo que lo que hemos visto es suficiente para hacernos una idea, aunque si queremos ofrecer una solución más acabada y profesional nos faltarían algunos métodos, como ContainsId() o ContainsToken() para determinar si un ítem ya existe, o RemoveById() o RemoveByToken() para eliminar de los dos diccionarios un elemento. También podríamos introducir comprobaciones en el método Add() para evitar la entrada de claves duplicadas. Pero todo esto es trivial, así que os lo dejo como deberes ;)

Publicado en Variable not found.

2 Comentarios:

Javier dijo...

Si no se puede garantizar que ambas claves tienen espacios de valores distintos se podría añadir un prefijo a la clave antes de almacenar y recuperar, algo así:

public void Add(Friend friend)
{
_friends[PFX_ID + friend.Id.ToString()] = friend;
_friends[PFX_TOKEN + friend.Token] = friend;
}

public Friend? GetById(int id)
=> _friends.TryGetValue(PFX_ID + id.ToString(), out var found) ? found : null;

public Friend? GetByToken(string token)
=> _friends.TryGetValue(PFX_TOKEN + token, out var found) ? found : null;

José María Aguilar dijo...

En efecto Javier, sería una forma de conseguirlo.

Muchas gracias por puntualizarlo!