Listas
En resumen: Una lista es una colección que crece y se encoge sola; List<T> guarda los elementos en orden como un array, y List<string> dice de qué tipo son.
Problema
El carrito está lleno
Estás escribiendo el carrito de compras de una tienda en línea, y guardas los productos en un array. Pero un array tiene un tamaño que no cambia. Cuando el cliente añade un cuarto producto, creas un array más grande y copias todo. Cuando quita un producto, queda un hueco:
string[] cart = { "Pan", "Leche", "Café" };
// Un cuarto producto: el array está lleno, así que creamos uno más grande y copiamos.string[] bigger = new string[cart.Length + 1];for (int i = 0; i < cart.Length; i++){ bigger[i] = cart[i];}bigger[cart.Length] = "Mantequilla";cart = bigger;
// El cliente quita la leche.cart[1] = null;
foreach (string item in cart){ Console.WriteLine(item);}Pan
CaféMantequillanull: Console.WriteLine lo imprime como una línea vacía. Y el número de productos ya no es cart.Length.Es mucho código para algo que toda tienda hace todo el tiempo.
La idea
List<T>: un array que se cuida solo
C# tiene una clase lista para esto: List<T>. Guarda los elementos en un array, pero copia y desplaza sola. Una colección así se llama lista:
List<string> cart = new List<string>();cart.Add("Pan");cart.Add("Leche");cart.Add("Café");cart.Add("Mantequilla");cart.Remove("Leche");Console.WriteLine(string.Join(", ", cart));Console.WriteLine("Productos: " + cart.Count);Pan, Café, MantequillaProductos: 3List<T>está en el espacio de nombresSystem.Collections.Generic. No escribes unusingpara él: los proyectos nuevos lo añaden solos, comousing System;.Addañade al final, yRemovequita y cierra el hueco: «Café» pasa al lugar de «Leche».Countes cuántos elementos hay ahora mismo. No lleva paréntesis, porque es una propiedad. No esLength, porque una lista no es un array.Console.WriteLine(cart)no imprime los elementos, solo el nombre del tipo:
List<string> cart = new List<string>();cart.Add("Pan");Console.WriteLine(cart);System.Collections.Generic.List`1[System.String]Por eso string.Join(", ", cart) une los elementos en un solo texto, separados por comas.
Esto es lo que hace List<T> por dentro. El número entre paréntesis es el tamaño inicial del array. Sin él, el array está vacío, y con el primer Add recibe 4 lugares. Aquí es 3, para que veas qué pasa cuando el array se llena:
List<string> cart = new List<string>(3);cart.Add("Pan");cart.Add("Leche");cart.Add("Café");cart.Add("Mantequilla");cart.Remove("Leche");Console.WriteLine(string.Join(", ", cart));Console.WriteLine("Productos: " + cart.Count);Paso a paso, en palabras
- cart apunta a un objeto List<string>. Dentro hay un array _items con 3 lugares y un contador _size.
- Add pone cada elemento en el primer lugar libre y aumenta _size.
- El array está lleno. List<T> crea uno nuevo con 6 lugares, el doble, y copia en él los tres elementos.
- Ahora _items apunta al array nuevo, y el viejo ya no lo necesita nadie. «Mantequilla» va al lugar libre.
- Remove quita «Leche», y todo lo que viene después se mueve un lugar a la izquierda. No queda ningún hueco.
- Desde fuera solo ves una lista de tres productos: List<T> se encarga de copiar y de mover. Capacity te dice cuántos lugares tiene.
Más precisamente, para curiosos
- La propiedad
Capacityte dice cuántos lugares tiene el array de dentro. Cuando se llena,List<T>crea un array nuevo con el doble de lugares:
List<string> cart = new List<string>();Console.WriteLine(cart.Capacity);cart.Add("Pan");Console.WriteLine(cart.Capacity);for (int i = 0; i < 4; i++){ cart.Add("Leche");}Console.WriteLine(cart.Count + " " + cart.Capacity);045 8La idea
Qué va entre los corchetes angulares
List<string> significa «una lista de texto». El tipo entre los corchetes angulares se llama parámetro de tipo (generics). Le dice al compilador qué hay en la lista, y el compilador lo hace cumplir:
List<string> cart = new List<string>();cart.Add("Pan");cart.Add(5);error CS1503: Argument 1: cannot convert from 'int' to 'string'Por eso cart[0] devuelve directamente un string, sin ninguna conversión.
A la derecha, el tipo se escribe otra vez: new List<string>(). También hay una forma corta, new(). Entonces el compilador toma el tipo del lado izquierdo:
List<string> cart = new();cart.Add("Pan");Console.WriteLine(cart[0]);PanEn C#, la variable suele tener como tipo la propia clase, List<string>. La lista también tiene una interfaz, IList<string>, con una I delante como toda interfaz en C#. Dice qué puede hacer una lista, y List<string> es una forma de hacerlo:
IList<string> cart = new List<string>();cart.Add("Pan");Console.WriteLine(cart[0]);PanEs el polimorfismo del módulo «Herencia y polimorfismo». Verás la interfaz sobre todo como tipo de un parámetro.
La idea
Índices: [i], Insert, IndexOf
Una lista tiene índices como un array, del 0 a Count - 1, y los escribes con los mismos [ ]:
List<string> tasks = new List<string>();tasks.Add("Factura de marzo");tasks.Add("Llamar al banco");tasks.Add("Informe del almacén");
Console.WriteLine(tasks[0]);tasks[1] = "Reunión en el banco";tasks.Insert(0, "Café con el equipo");Console.WriteLine(string.Join(", ", tasks));Console.WriteLine(tasks.IndexOf("Informe del almacén"));Console.WriteLine(tasks.Contains("Llamar al banco"));Factura de marzoCafé con el equipo, Factura de marzo, Reunión en el banco, Informe del almacén3Falsetasks[i]lee, ytasks[i] = xreemplaza.Insert(i, x)inserta en el índiceiy mueve el resto a la derecha.IndexOf(x)busca y devuelve el índice o-1, yContains(x)solo dice si está.
Más precisamente, para curiosos
- Un índice fuera de la lista detiene el programa. La excepción es distinta de la de un array:
ArgumentOutOfRangeException, noIndexOutOfRangeException.
Unhandled exception. System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection. (Parameter 'index')^1cuenta desde el final:tasks[^1]es la última tarea, «Informe del almacén».ContainseIndexOfcomparan conEquals. Para el texto eso significa «los mismos caracteres», así que encuentran un texto igual, no solo el mismo objeto.- Insertar y quitar al principio es lento en una lista grande: cada elemento después del índice se mueve un lugar. Con un millón de elementos, son un millón de movimientos.
Pruébalo
¿Quién sigue?
La lista de pacientes que esperan en el médico. Juan entra, y Ana tiene cita, así que ocupa el segundo lugar.
¿Qué imprimirá el programa?
List<string> waiting = new List<string>();waiting.Add("Juan");waiting.Add("María");waiting.Add("Pedro");waiting.RemoveAt(0);waiting.Insert(1, "Ana");Console.WriteLine(string.Join(", ", waiting) + " " + waiting.Count);RemoveAt(0) quita a Juan, y María pasa a ser la primera, en el índice 0. Insert(1, "Ana") inserta a Ana en el índice 1 y empuja a Pedro a la derecha. Quedan tres.
La idea
Recorrer los elementos
Recorres una lista como un array: con foreach cuando solo necesitas los elementos, o con for y un índice cuando también necesitas el número:
List<string> cart = new List<string>();cart.Add("Pan");cart.Add("Café");cart.Add("Mantequilla");
foreach (string item in cart){ Console.WriteLine("- " + item);}for (int i = 0; i < cart.Count; i++){ Console.WriteLine((i + 1) + ". " + cart[i]);}- Pan- Café- Mantequilla1. Pan2. Café3. MantequillaMás precisamente, para curiosos
- No quites elementos de una lista mientras la recorres con
foreach. El programa se detiene:
foreach (string item in cart){ if (item == "Pan") { cart.Remove(item); }}Unhandled exception. System.InvalidOperationException: Collection was modified; enumeration operation may not execute.La excepción llega siempre, incluso cuando quitas el penúltimo elemento: en el paso siguiente, foreach comprueba si la lista cambió.
- Si tienes que quitar, recorre con un índice de atrás hacia delante. Así, quitar no desplaza los elementos que todavía no has comprobado:
List<string> cart = new List<string>();cart.Add("Pan");cart.Add("Leche");cart.Add("Pan");for (int i = cart.Count - 1; i >= 0; i--){ if (cart[i] == "Pan") { cart.RemoveAt(i); }}Console.WriteLine(string.Join(", ", cart));LecheRemoveAll.La idea
Números en una lista: List<int>
Una lista de puntuaciones es una List<int>. Los corchetes angulares también aceptan un tipo primitivo como int, double o bool, así que el número está en la lista tal cual:
List<int> scores = new List<int>();scores.Add(85);scores.Add(64);scores.Add(91);
int total = 0;foreach (int score in scores){ total += score;}Console.WriteLine("Media: " + (double) total / scores.Count);Media: 80(double) convierte la suma en un número decimal, para que la división no sea una división entera. C# imprime 80, no 80.0: un número decimal sin parte decimal sale sin punto.
Más precisamente, para curiosos
- C# también tiene boxing. Cuando un tipo primitivo va a una variable de tipo
object, C# lo mete en un objeto. Y==entre dos valoresobjectcomprueba si apuntan al mismo objeto:
object a = 127;object b = 127;Console.WriteLine(a == b);Console.WriteLine(a.Equals(b));
int c = 127;int d = 127;Console.WriteLine(c == d);FalseTrueTrueLos dos 127 se envuelven por separado, así que hay dos objetos. Equals compara los valores.
- La antigua clase
ArrayList(sin corchetes angulares, deSystem.Collections) guarda todo comoobject. Acepta tanto un número como un texto, y envuelve cada número:
ArrayList old = new ArrayList();old.Add(85);old.Add("Pan");Console.WriteLine(old.Count);2Por eso hoy se usa List<int>.
- Un
intno puede sernull. Un número que puede faltar se escribeint?:
int? discount = null;Console.WriteLine(discount.HasValue);discount = 10;Console.WriteLine(discount.HasValue + " " + discount);FalseTrue 10Pruébalo
Encuentra el error
Un teatro guarda los asientos ocupados en una lista. Un cliente deja el asiento 2, y el programa debería imprimir 12, 7, 5. En cambio imprime 12, 2, 5. ¿Qué línea tiene el error?
Elige la línea con el error.
RemoveAt(i) quita por índice: RemoveAt(2) quita el elemento del índice 2, que es el asiento 7. Remove quita por valor: seats.Remove(2). En C# los dos métodos tienen nombres distintos, así que Remove(2) siempre significa el valor 2. Pero con números es fácil confundir cuál de los dos necesitas, porque tanto el índice como el valor son números.Más precisamente, para curiosos
- Si el valor no está,
Removeno detiene el programa y no quita nada. Devuelvefalse:
List<int> seats = new List<int> { 12, 7, 5 };Console.WriteLine(seats.Remove(2));Console.WriteLine(string.Join(", ", seats));False12, 7, 5La idea
Ordenar y una lista de solo lectura
Sort() ordena la lista en el sitio: el texto en orden alfabético, los números por tamaño.
List<string> cities = new List<string>();cities.Add("Sevilla");cities.Add("Madrid");cities.Add("Valencia");cities.Sort();Console.WriteLine(string.Join(", ", cities));
List<int> prices = new List<int> { 30, 5, 12 };prices.Sort();Console.WriteLine(string.Join(", ", prices));Madrid, Sevilla, Valencia5, 12, 30new List<int> { 30, 5, 12 } crea una lista con los valores que enumeras. Es una lista normal, y puedes cambiarla.
Una lista que no cambia se declara con la interfaz IReadOnlyList<T>. Solo tiene los miembros que leen: el indexador y Count. Así que el compilador no te deja añadir, quitar ni ordenar:
IReadOnlyList<string> days = new List<string> { "lun", "mar", "mié" };days.Add("jue");error CS1061: 'IReadOnlyList<string>' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'IReadOnlyList<string>' could be found (are you missing a using directive or an assembly reference?)IReadOnlyList no crea una copia. Solo oculta los miembros que cambian la lista. Si alguien tiene la propia List, su cambio también se ve a través de days:
List<string> week = new List<string> { "lun", "mar", "mié" };IReadOnlyList<string> days = week;week.Add("jue");Console.WriteLine(days.Count);4Más precisamente, para curiosos
Sort()ordena el texto según las reglas del idioma configurado en la computadora (la cultura). Las mayúsculas y las minúsculas no se separan, así que «manzana» va antes que «Pera». Si pasasStringComparer.Ordinal, el texto se ordena por los códigos de sus caracteres, y las mayúsculas van antes que las minúsculas:
List<string> fruit = new List<string> { "manzana", "Pera", "banana", "Uva" };fruit.Sort();Console.WriteLine(string.Join(", ", fruit));fruit.Sort(StringComparer.Ordinal);Console.WriteLine(string.Join(", ", fruit));banana, manzana, Pera, UvaPera, Uva, banana, manzana- Cómo ordenar tus propios objetos, por ejemplo productos por precio, lo verás en la lección «Clases anidadas».
Ejercicio
Listas en clases
private de la clase, y Main solo llama a sus métodos.1. Una estantería. Una clase BookShelf con un método AddBook(string title) que pone el libro al final, y TakeBook(), que devuelve y quita el primer libro (o null si la estantería está vacía). Añade una propiedad Count y ToString(). En Main, añade «Bajo el yugo», «Tabaco» y «El candelabro de hierro», toma un libro e imprime el resto.
Tomado: Bajo el yugo2 quedan: [Tabaco, El candelabro de hierro]2. Un registro de eventos. Una clase Event con una fecha y una descripción, y una clase EventLog con los métodos AddEvent(string date, string description) y GetEventsBetween(string from, string to), que devuelve una lista con los eventos entre las dos fechas, incluidas. Las fechas son texto con la forma 2026-03-31. Con esta forma, el texto se ordena igual que las fechas, así que string.CompareOrdinal(a, b) las compara correctamente. Devuelve un número negativo si a va antes que b, 0 si son iguales y un número positivo si a va después de b. Añade cuatro eventos: 2026-01-15 «Inventario anual», 2026-03-31 «Informe del primer trimestre», 2026-05-20 «Nuevo proveedor de café», 2026-09-01 «Nuevo curso escolar». Imprime los del 2026-03-01 al 2026-06-30.
2026-03-31 Informe del primer trimestre2026-05-20 Nuevo proveedor de café3. Un cuaderno. Una clase Note con un título y un contenido, y una clase Notebook con los métodos AddNote, FindNote(string title) (devuelve la nota o null), DeleteNote(string title) (devuelve true si borró algo) y una propiedad Count. Añade las notas «POO», «Colecciones» y «SQL», busca «Colecciones», borra «POO» y comprueba que ya no está.
Colecciones: List<T> crece solaBorrada: POONotas: 2No hay ninguna nota de POO.4. Cálculo mental. Una clase Question con un texto y la respuesta correcta, y una clase MathGame con una lista de preguntas, un método AddQuestion y un método Start(). Start hace las preguntas una a una, lee la respuesta y al final dice cuántas fueron correctas. Las preguntas son 2 + 2, 3 * 4 y 15 - 9.
41272 + 2 = ¡Correcto!3 * 4 = ¡Correcto!15 - 9 = Incorrecto, la respuesta es 6.Puntuación: 2 de 3Más tareas
- Sin repetidos. Lee 6 nombres y reúnelos en una lista, saltándote los que ya están. Para la entrada Juan, María, Juan, Ana, María, Pedro la salida es
Juan, María, Ana, Pedro. - El producto más caro. Una List<double> guarda precios. Encuentra el más alto y su índice, sin
Max. - La lista al revés. Escribe un método que devuelva una lista nueva con los mismos elementos en orden inverso. No cambies el original.
- Una lista de la compra. Lee las órdenes
add milk,remove milk,listyend, y guarda la lista entre ellas. La orden de quitar un producto que no está dice «No existe ese producto.».
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Encapsulamiento · Polimorfismo · Interfaces · Excepciones
- Continúa en: Pila, cola y lista enlazada · Dictionary y HashSet · Códigos hash y funciones hash · Clases anidadas
- Ejercicios sobre el tema: Colecciones