Ir al contenido
wannacode

Listas, diccionarios y LINQ

En resumen: Una List guarda elementos en orden y crece con Add, un Dictionary guarda pares de clave y valor, y LINQ con Where y Select consulta los datos con lambdas.

Problema

El carrito crece

Un cliente de la tienda en línea pone en el carrito café, té, leche y luego también filtros. Después quita la leche. No sabes de antemano cuántos productos va a comprar. Pero un array quiere su tamaño ya en el new:

string[] cart = new string[3];
cart[0] = "Café en grano";
cart[1] = "Té verde";
cart[2] = "Leche para café";
try
{
cart[3] = "Filtros de café";
}
catch (IndexOutOfRangeException e)
{
Console.WriteLine("No cabe: " + e.Message);
}
Salida
No cabe: Index was outside the bounds of the array.

Hay un segundo problema. El almacén guarda los productos por código, como C-101. Para encontrar la cantidad de un código en un array, recorres el array con un bucle cada vez. Con 10 000 productos y 100 pedidos por minuto, son muchos bucles para nada.

Míralo

List<T>: un array que crece

Una lista (List) guarda elementos en orden, como un array, pero crece con Add y se reduce con Remove:

List<string> cart = new List<string>();
cart.Add("Café en grano");
cart.Add("Té verde");
cart.Add("Leche para café");
cart.Add("Filtros de café");
Console.WriteLine("En el carrito: " + cart.Count);
Console.WriteLine("El primero: " + cart[0]);
cart.Remove("Leche para café");
Console.WriteLine("¿Queda leche? " + cart.Contains("Leche para café"));
foreach (string item in cart)
{
Console.WriteLine("- " + item);
}
Salida
En el carrito: 4
El primero: Café en grano
¿Queda leche? False
- Café en grano
- Té verde
- Filtros de café
  • En List<string>, los corchetes angulares dicen qué hay en la lista: cadenas. List<int> es una lista de números enteros, y List<Product> es una lista de productos. El tipo entre los corchetes se llama parámetro de tipo. Por eso escribimos List<T>, donde T es «el tipo que elijas».
  • Count es el número de elementos (en un array es Length). El índice funciona como en un array: cart[0] es el primero.
  • Remove quita el elemento si está, y Contains dice si está. foreach los recorre todos.

En este módulo los programas no tienen el envoltorio class Program y Main: las líneas están directamente en el archivo. Son las instrucciones de nivel superior (top-level statements) de la lección «Preparar tu computadora»: el compilador pone Main alrededor de ellas por su cuenta. Las clases, cuando las hay, van después de esas líneas.

Más precisamente, para curiosos
  • Puedes llenar una lista en el momento de crearla: new List<int> { 2, 1, 3 }. Hay más métodos: Insert(índice, valor) pone un valor en una posición, RemoveAt(índice) quita por posición y Clear() vacía la lista.
List<int> quantities = new List<int> { 2, 1, 3 };
quantities.Insert(0, 5);
quantities.RemoveAt(2);
Console.WriteLine(string.Join(", ", quantities));
quantities.Clear();
Console.WriteLine(quantities.Count);
Salida
5, 2, 3
0
  • string.Join(", ", lista) une los elementos en una sola cadena, con un separador entre ellos.
  • Por dentro, una lista es un array con algo de espacio de reserva. Cuando se llena, List crea un array nuevo, el doble de grande, y copia los elementos. Por eso Add suele ser rápido, y tú no tienes que pensar en el tamaño.
  • Si usas un índice fuera de la lista, cart[10], la excepción es ArgumentOutOfRangeException, no IndexOutOfRangeException como en un array.

Pruébalo

Qué queda en la lista

¿Qué imprimirá el programa?

List<int> quantities = new List<int> { 2, 5, 2, 1 };
quantities.Remove(2);
quantities.Add(3);
Console.WriteLine(quantities.Count + " " + quantities[0] + " " + quantities[3]);
Respuesta

Míralo

Un diccionario: búsqueda por código

Un diccionario (Dictionary) guarda pares de clave → valor. Aquí la clave es el código del producto, y el valor es la cantidad en el almacén. Encontrar una clave es casi instantáneo, sin bucle, haya los productos que haya:

Dictionary<string, int> stock = new Dictionary<string, int>();
stock["C-101"] = 12;
stock["T-205"] = 30;
stock["C-101"] = stock["C-101"] - 2;
Console.WriteLine("Café en almacén: " + stock["C-101"]);
Console.WriteLine("¿Hay X-999? " + stock.ContainsKey("X-999"));
if (stock.TryGetValue("T-205", out int tea))
{
Console.WriteLine("Té en almacén: " + tea);
}
try
{
Console.WriteLine(stock["X-999"]);
}
catch (KeyNotFoundException)
{
Console.WriteLine("No hay ningún producto con código X-999");
}
foreach (KeyValuePair<string, int> pair in stock)
{
Console.WriteLine(pair.Key + ": " + pair.Value + " uds.");
}
Salida
Café en almacén: 10
¿Hay X-999? False
Té en almacén: 30
No hay ningún producto con código X-999
C-101: 10 uds.
T-205: 30 uds.
  • Dictionary<string, int>: las claves son cadenas y los valores son números enteros.
  • stock["C-101"] = 12 escribe. Si la clave ya está, el valor se reemplaza. stock["C-101"] lee.
  • Leer una clave que no está lanza KeyNotFoundException. La capturas con try/catch, como en el módulo «Errores y excepciones». Es mejor no llegar a eso: ContainsKey dice si la clave está, y TryGetValue comprueba y lee de una vez. El valor llega a través de out, como con int.TryParse.
  • foreach recorre los pares. Cada uno es un KeyValuePair con Key y Value.
Más precisamente, para curiosos
  • Add(clave, valor) también escribe, pero lanza ArgumentException si la clave ya está. Así no sobrescribes un precio por error. Puedes llenar un diccionario al crearlo, con pares entre llaves:
Dictionary<string, decimal> prices = new Dictionary<string, decimal>
{
{ "C-101", 18.90m },
{ "T-205", 6.40m },
};
try
{
prices.Add("C-101", 19.50m);
}
catch (ArgumentException)
{
Console.WriteLine("C-101 ya tiene precio: " + prices["C-101"] + " €");
}
prices.Remove("T-205");
Console.WriteLine("Productos con precio: " + prices.Count);
Salida
C-101 ya tiene precio: 18.90 €
Productos con precio: 1
  • Las claves son únicas, los valores no. Dos productos pueden tener los dos una cantidad de 30, pero el código C-101 es solo uno.
  • El orden en que foreach te da los pares no está garantizado. Hoy es el orden en que los añadiste, si no has quitado nada, pero no cuentes con ello. Si necesitas un orden, ordena las claves.
  • Un diccionario encuentra una clave rápido porque calcula a partir de ella un número (un hash) y por él sabe dónde buscar. Por eso una clave no debe cambiar mientras está en el diccionario.

Pruébalo

Un código que no está

El programa debería mostrar el precio del producto con código C-102. En cambio, se detiene con una excepción. ¿En qué línea está el problema?

Elige la línea con el error.

La idea

Una regla como valor: la lambda

Encontrar los precios de 50 € en adelante es un bucle con un if:

List<int> prices = new List<int> { 12, 75, 40, 130, 58 };
List<int> expensive = new List<int>();
foreach (int p in prices)
{
if (p >= 50)
{
expensive.Add(p);
}
}
Console.WriteLine(string.Join(", ", expensive));
Salida
75, 130, 58

Mañana necesitarás los productos de menos de 10 €, y luego solo los tés. El bucle es el mismo, solo cambia la condición del if. Sería práctico pasar la condición como valor a un método que haga el bucle por ti.

Una condición así se escribe como una lambda: p => p >= 50. Léela como «para cada p, da p >= 50». A la izquierda de => está el parámetro, a la derecha el resultado. Una lambda es un método corto sin nombre, sin return y sin tipos: C# los deduce por su cuenta.

Más precisamente, para curiosos

Una lambda tiene un tipo, como todo valor. Func<int, bool> es «un método que recibe un int y devuelve un bool». Puedes guardarla en una variable y llamarla como un método. Una lambda también puede tener un cuerpo entre llaves con return, cuando una sola expresión no basta:

Func<int, bool> expensive = p => p >= 50;
Console.WriteLine(expensive(75) + " " + expensive(12));
Func<decimal, bool> overWithVat = p =>
{
decimal withVat = p * 1.2m;
return withVat > 50;
};
Console.WriteLine(overWithVat(45.00m));
Salida
True False
True

Míralo

Where y Select

List<int> prices = new List<int> { 12, 75, 40, 130, 58 };
List<string> labels = prices
.Where(p => p >= 50)
.Select(p => p + " €")
.ToList();
Console.WriteLine(string.Join(", ", labels));
Paso a paso, en palabras
  1. Cinco precios. La consulta solo prepara el camino: todavía no ha pasado nada.
  2. 12 no pasa p >= 50 y se queda fuera. Nunca llega a Select.
  3. 75 pasa Where, Select lo convierte en el texto «75 €», ToList() lo reúne.
  4. 40 también se queda fuera. Cada elemento recorre todo el camino antes de que salga el siguiente.
  5. 130 y 58 pasan. La lista prices no cambia.
  6. El resultado es una lista nueva con tres cadenas.

Los métodos Where y Select vienen de LINQ (Language Integrated Query): consultas sobre los datos, integradas en el lenguaje. Funcionan con cualquier lista y cualquier array:

  • Where(p => p >= 50) deja pasar solo los elementos para los que la lambda da true. Es el bucle con el if de la tarjeta anterior.
  • Select(p => p + " €") convierte cada elemento en otra cosa, aquí en una cadena.
  • ToList() reúne el resultado en una List nueva. La lista prices no cambia.

Los elementos pasan de uno en uno: el 12 se queda fuera ya en Where y nunca llega a Select.

Míralo

Los productos de la tienda

Un catálogo real es una lista de objetos. La clase Product solo tiene propiedades, así que los objetos se crean con las propiedades entre llaves: new Product { Id = 1, Name = "…" }. Es lo mismo que new Product() y después p.Id = 1; para cada propiedad.

List<Product> catalog = new List<Product>
{
new Product { Id = 1, Name = "Espresso en grano", Price = 24.90m, Category = "café" },
new Product { Id = 2, Name = "Té verde sencha", Price = 9.80m, Category = "té" },
new Product { Id = 3, Name = "Colombia en grano", Price = 31.50m, Category = "café" },
new Product { Id = 4, Name = "Menta", Price = 4.20m, Category = "té" },
new Product { Id = 5, Name = "Molinillo de café", Price = 64.00m, Category = "aparatos" },
};
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Category { get; set; }
}

La lambda recibe un producto entero, así que la condición lee sus propiedades:

List<Product> coffee = catalog.Where(p => p.Category == "café").ToList();
Console.WriteLine("Cafés: " + coffee.Count);
List<string> byPrice = catalog
.OrderByDescending(p => p.Price)
.Select(p => p.Name + ": " + p.Price + " €")
.ToList();
foreach (string line in byPrice)
{
Console.WriteLine(line);
}
Salida
Cafés: 2
Molinillo de café: 64.00 €
Colombia en grano: 31.50 €
Espresso en grano: 24.90 €
Té verde sencha: 9.80 €
Menta: 4.20 €
  • Where(p => p.Category == "café") deja solo los cafés.
  • OrderByDescending(p => p.Price) ordena por precio, del más alto al más bajo. OrderBy ordena del más bajo. La lambda dice por qué se ordena.
  • Los métodos se encadenan uno tras otro: cada uno recibe el resultado del anterior.

El catálogo es el mismo. ¿Qué imprimirá?

List<string> names = catalog
.Where(p => p.Price < 30)
.OrderBy(p => p.Price)
.Select(p => p.Name)
.ToList();
Console.WriteLine(names.Count + ": " + names[0]);
Respuesta

Míralo

Una respuesta de toda la lista

Algunos métodos de LINQ no devuelven una lista, sino un solo valor: una suma, una cantidad, «hay alguno», un producto:

Console.WriteLine("Total: " + catalog.Sum(p => p.Price) + " €");
Console.WriteLine("Media: " + catalog.Average(p => p.Price) + " €");
Console.WriteLine("Menos de 5 €: " + catalog.Count(p => p.Price < 5));
Console.WriteLine("¿Hay aparatos? " + catalog.Any(p => p.Category == "aparatos"));
Product tea = catalog.First(p => p.Category == "té");
Console.WriteLine("El primer té: " + tea.Name);
Product cocoa = catalog.FirstOrDefault(p => p.Category == "cacao");
Console.WriteLine(cocoa == null ? "No hay cacao" : cocoa.Name);
Salida
Total: 134.40 €
Media: 26.88 €
Menos de 5 €: 1
¿Hay aparatos? True
El primer té: Té verde sencha
No hay cacao
  • Sum y Average suman y promedian lo que da la lambda.
  • Count(regla) cuenta los que cumplen la regla, y Any(regla) dice si hay al menos uno.
  • First(regla) devuelve el primero que la cumple, y si no hay ninguno, lanza InvalidOperationException. FirstOrDefault devuelve null en el mismo caso. Úsalo cuando sea normal que no haya resultado, y comprueba si es null.

¿Qué método da el valor total del catálogo?

decimal total = catalog.(p => p.Price);
Más precisamente, para curiosos
  • GroupBy divide los elementos en grupos según una clave. Cada grupo tiene una Key y se comporta como una lista, así que Count y Sum funcionan con él. El tipo del grupo es largo (IGrouping<string, Product>), así que aquí viene bien var:
foreach (var group in catalog.GroupBy(p => p.Category))
{
Console.WriteLine(group.Key + ": cantidad " + group.Count() + ", total " + group.Sum(p => p.Price) + " €");
}
Salida
café: cantidad 2, total 56.40 €
té: cantidad 2, total 14.00 €
aparatos: cantidad 1, total 64.00 €
  • La misma consulta también se puede escribir con sintaxis de consulta, que se parece a SQL. El compilador la convierte en Where, OrderBy y Select:
List<string> teas = (from p in catalog
where p.Category == "té"
orderby p.Price
select p.Name).ToList();
Console.WriteLine(string.Join(", ", teas));
Salida
Menta, Té verde sencha
  • Ejecución diferida. Where y Select no recorren nada hasta que alguien pide el resultado: ToList, foreach, Sum, string.Join. Hasta entonces la consulta es solo una receta, de tipo IEnumerable<int> («algo que se puede recorrer»). Por eso aquí el 130 entra en el resultado, aunque se añadió después de Where:
List<int> prices = new List<int> { 12, 75 };
IEnumerable<int> expensive = prices.Where(p => p >= 50);
prices.Add(130);
Console.WriteLine(string.Join(", ", expensive));
Salida
75, 130
  • Si necesitas el resultado tal como está en un momento dado, llama a ToList() enseguida: entonces la lista ya está hecha, y los cambios posteriores no la afectan.
  • Aquí los precios son decimal: con dinero, double da errores de redondeo. Sum conserva los dos decimales: 134.40. Los números se imprimen con punto porque la comprobación del código se ejecuta sin configuración regional española; en una computadora configurada en español verías 134,40.

Ejercicio

Carrito, almacén e informe

Resuélvelos en tu editor. Usa la clase Product de la lección.

1. Carrito. Pon en una List<Product> espresso en grano (24,90 €), menta (4,20 €) y un molinillo de café (64,00 €). Imprime la cantidad y el total. Luego busca el molinillo por Id con First, quítalo e imprime otra vez:

Salida
Productos: 3, total 93.10 €
Quitado: Molinillo de café
Productos: 2, total 29.10 €

2. Almacén. Un diccionario de código → cantidad: C-101 12 uds., T-205 30 uds., G-300 2 uds. Escribe un método Sell(stock, code, quantity) que reduzca la cantidad y devuelva un mensaje con el resultado. Si el código no está o la cantidad no alcanza, el diccionario no cambia. Vende 5 de C-101, 3 de G-300 y 1 de X-999, y luego imprime el almacén:

Salida
C-101: vendidos 5, quedan 7
G-300: solo quedan 2
No existe el producto X-999
C-101: 7
T-205: 30
G-300: 2

3. Informe. Con el catálogo de la lección, imprime los tres productos más caros y la suma de los precios por categoría. Take(3) toma los tres primeros elementos. Para las sumas, usa un diccionario de categoría → suma (o GroupBy de «Más precisamente»):

Salida
Los tres más caros:
Molinillo de café 64.00 €
Colombia en grano 31.50 €
Espresso en grano 24.90 €
Por categoría:
café: 56.40 €
té: 14.00 €
aparatos: 64.00 €

Test

Ponte a prueba

1. ¿Por qué para el carrito de una tienda List<Product> es más práctica que un array Product[]?
2. stock es un Dictionary<string, int> sin la clave "X-999". ¿Qué hace stock["X-999"]?
3. ¿Qué significa p => p.Price > 50?
4. En el catálogo no hay ningún producto de la categoría «cacao». ¿Qué devuelve catalog.FirstOrDefault(p => p.Category == "cacao")?
5. ¿Qué diferencia hay entre Count y Count(p => p.Price < 5) en una lista?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·