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);}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);}En el carrito: 4El 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, yList<Product>es una lista de productos. El tipo entre los corchetes se llama parámetro de tipo. Por eso escribimosList<T>, dondeTes «el tipo que elijas». Countes el número de elementos (en un array esLength). El índice funciona como en un array:cart[0]es el primero.Removequita el elemento si está, yContainsdice si está.foreachlos 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 yClear()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);5, 2, 30string.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,
Listcrea un array nuevo, el doble de grande, y copia los elementos. Por esoAddsuele 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 esArgumentOutOfRangeException, noIndexOutOfRangeExceptioncomo 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]);Remove(2) quita solo el primer 2. Queda { 5, 2, 1 }, y luego Add(3) lo convierte en { 5, 2, 1, 3 }. Cuatro elementos: el primero es 5 y el cuarto es 3.
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.");}Café en almacén: 10¿Hay X-999? FalseTé en almacén: 30No hay ningún producto con código X-999C-101: 10 uds.T-205: 30 uds.Dictionary<string, int>: las claves son cadenas y los valores son números enteros.stock["C-101"] = 12escribe. Si la clave ya está, el valor se reemplaza.stock["C-101"]lee.- Leer una clave que no está lanza
KeyNotFoundException. La capturas contry/catch, como en el módulo «Errores y excepciones». Es mejor no llegar a eso:ContainsKeydice si la clave está, yTryGetValuecomprueba y lee de una vez. El valor llega a través deout, como conint.TryParse. foreachrecorre los pares. Cada uno es unKeyValuePairconKeyyValue.
Más precisamente, para curiosos
Add(clave, valor)también escribe, pero lanzaArgumentExceptionsi 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);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-101es solo uno. - El orden en que
foreachte 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 clave "C-102" no está en el diccionario, así que prices[code] lanza KeyNotFoundException. Cuando el código viene de fuera (del cliente, de un archivo), compruébalo: if (prices.TryGetValue(code, out decimal price)), y en el else imprime «No hay precio para C-102».
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));75, 130, 58Mañ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));True FalseTrueMí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
- Cinco precios. La consulta solo prepara el camino: todavía no ha pasado nada.
- 12 no pasa p >= 50 y se queda fuera. Nunca llega a Select.
- 75 pasa Where, Select lo convierte en el texto «75 €», ToList() lo reúne.
- 40 también se queda fuera. Cada elemento recorre todo el camino antes de que salga el siguiente.
- 130 y 58 pasan. La lista prices no cambia.
- 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 datrue. Es el bucle con elifde la tarjeta anterior.Select(p => p + " €")convierte cada elemento en otra cosa, aquí en una cadena.ToList()reúne el resultado en unaListnueva. La listapricesno 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);}Cafés: 2Molinillo 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.OrderByordena 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]);Hay tres productos de menos de 30 €: el espresso (24,90 €), el sencha (9,80 €) y la menta (4,20 €). OrderBy los ordena del más barato, así que la menta va primero.
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);Total: 134.40 €Media: 26.88 €Menos de 5 €: 1¿Hay aparatos? TrueEl primer té: Té verde senchaNo hay cacaoSumyAveragesuman y promedian lo que da la lambda.Count(regla)cuenta los que cumplen la regla, yAny(regla)dice si hay al menos uno.First(regla)devuelve el primero que la cumple, y si no hay ninguno, lanzaInvalidOperationException.FirstOrDefaultdevuelvenullen el mismo caso. Úsalo cuando sea normal que no haya resultado, y comprueba si esnull.
¿Qué método da el valor total del catálogo?
decimal total = catalog.(p => p.Price);Sum(p => p.Price) suma los precios de todos los productos: 134,40 €.
Más precisamente, para curiosos
GroupBydivide los elementos en grupos según una clave. Cada grupo tiene unaKeyy se comporta como una lista, así queCountySumfuncionan con él. El tipo del grupo es largo (IGrouping<string, Product>), así que aquí viene bienvar:
foreach (var group in catalog.GroupBy(p => p.Category)){ Console.WriteLine(group.Key + ": cantidad " + group.Count() + ", total " + group.Sum(p => p.Price) + " €");}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,OrderByySelect:
List<string> teas = (from p in catalog where p.Category == "té" orderby p.Price select p.Name).ToList();Console.WriteLine(string.Join(", ", teas));Menta, Té verde sencha- Ejecución diferida.
WhereySelectno recorren nada hasta que alguien pide el resultado:ToList,foreach,Sum,string.Join. Hasta entonces la consulta es solo una receta, de tipoIEnumerable<int>(«algo que se puede recorrer»). Por eso aquí el 130 entra en el resultado, aunque se añadió después deWhere:
List<int> prices = new List<int> { 12, 75 };IEnumerable<int> expensive = prices.Where(p => p >= 50);prices.Add(130);Console.WriteLine(string.Join(", ", expensive));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,doubleda errores de redondeo.Sumconserva 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:
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:
C-101: vendidos 5, quedan 7G-300: solo quedan 2No existe el producto X-999C-101: 7T-205: 30G-300: 23. 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»):
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
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Encapsulamiento · static: lo que comparten todos los objetos · Excepciones · try, catch y finally
- Continúa en: async y await · Cómo funciona internet · Un servidor web con Minimal API · MVC: modelo, vista, controlador
- Ejercicios sobre el tema: Web y escritorio