Ir al contenido
wannacode

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);
}
Salida
Pan
Café
Mantequilla
Ahora cada bucle sobre el carrito tiene que saltarse null: 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);
Salida
Pan, Café, Mantequilla
Productos: 3
  • List<T> está en el espacio de nombres System.Collections.Generic. No escribes un using para él: los proyectos nuevos lo añaden solos, como using System;.
  • Add añade al final, y Remove quita y cierra el hueco: «Café» pasa al lugar de «Leche».
  • Count es cuántos elementos hay ahora mismo. No lleva paréntesis, porque es una propiedad. No es Length, 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);
Salida
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
  1. cart apunta a un objeto List<string>. Dentro hay un array _items con 3 lugares y un contador _size.
  2. Add pone cada elemento en el primer lugar libre y aumenta _size.
  3. El array está lleno. List<T> crea uno nuevo con 6 lugares, el doble, y copia en él los tres elementos.
  4. Ahora _items apunta al array nuevo, y el viejo ya no lo necesita nadie. «Mantequilla» va al lugar libre.
  5. Remove quita «Leche», y todo lo que viene después se mueve un lugar a la izquierda. No queda ningún hueco.
  6. 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 Capacity te 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);
Salida
0
4
5 8

La 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 de compilación
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]);
Salida
Pan

En 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]);
Salida
Pan

Es 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"));
Salida
Factura de marzo
Café con el equipo, Factura de marzo, Reunión en el banco, Informe del almacén
3
False
  • tasks[i] lee, y tasks[i] = x reemplaza.
  • Insert(i, x) inserta en el índice i y mueve el resto a la derecha.
  • IndexOf(x) busca y devuelve el índice o -1, y Contains(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, no IndexOutOfRangeException.
tasks[3] con 3 tareas
Unhandled exception. System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection. (Parameter 'index')
  • ^1 cuenta desde el final: tasks[^1] es la última tarea, «Informe del almacén».
  • Contains e IndexOf comparan con Equals. 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);
Respuesta

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]);
}
Salida
- Pan
- Café
- Mantequilla
1. Pan
2. Café
3. Mantequilla
Má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);
}
}
Error
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));
Salida
Leche
En el módulo «C# moderno» verás una forma más corta: RemoveAll.

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);
Salida
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 valores object comprueba 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);
Salida
False
True
True

Los dos 127 se envuelven por separado, así que hay dos objetos. Equals compara los valores.

  • La antigua clase ArrayList (sin corchetes angulares, de System.Collections) guarda todo como object. 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);
Salida
2

Por eso hoy se usa List<int>.

  • Un int no puede ser null. Un número que puede faltar se escribe int?:
int? discount = null;
Console.WriteLine(discount.HasValue);
discount = 10;
Console.WriteLine(discount.HasValue + " " + discount);
Salida
False
True 10

Prué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.

Más precisamente, para curiosos
  • Si el valor no está, Remove no detiene el programa y no quita nada. Devuelve false:
List<int> seats = new List<int> { 12, 7, 5 };
Console.WriteLine(seats.Remove(2));
Console.WriteLine(string.Join(", ", seats));
Salida
False
12, 7, 5

La 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));
Salida
Madrid, Sevilla, Valencia
5, 12, 30

new 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 de compilación
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);
Salida
4
Má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 pasas StringComparer.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));
Salida
banana, manzana, Pera, Uva
Pera, 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

Resuelve las tareas en tu editor. La lista es un campo 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.

Salida
Tomado: Bajo el yugo
2 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.

Salida
2026-03-31 Informe del primer trimestre
2026-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á.

Salida
Colecciones: List<T> crece sola
Borrada: POO
Notas: 2
No 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.

Entrada
4
12
7
Salida
2 + 2 = ¡Correcto!
3 * 4 = ¡Correcto!
15 - 9 = Incorrecto, la respuesta es 6.
Puntuación: 2 de 3

Má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, list y end, y guarda la lista entre ellas. La orden de quitar un producto que no está dice «No existe ese producto.».

Test

Ponte a prueba

1. ¿Por qué una List<T> es más práctica que un array para un carrito de compras?
2. ¿Qué dice <string> en List<string> cart?
3. List<int> seats contiene 12, 2, 7, 5. ¿Qué queda después de seats.Remove(2)?
4. ¿Puedes escribir List<int>?
5. IReadOnlyList<string> days = new List<string> { "lun", "mar" }; ¿Qué pasa con days.Add("mié")?

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