Control y colecciones en C#

Lección 3 de 8. Un programa casi nunca va en línea recta: decide, repite y guarda datos en colecciones. Esta lección cubre las dos cosas: if y el switch moderno con patrones, los bucles, y las colecciones que usarás todos los días, los arrays, List, Dictionary y HashSet.

Decidir: if y else

El if de C# es el de toda la familia de C, Java o JavaScript: una condición entre paréntesis y un bloque entre llaves. La diferencia está en lo que acepta como condición: solo un bool. Un número, un texto o un objeto no valen como «verdadero» o «falso», como pasa en JavaScript o Python.

int temperatura = 31;

if (temperatura >= 30)
{
    Console.WriteLine("Hace calor");
}
else if (temperatura >= 15)
{
    Console.WriteLine("Se está bien");
}
else
{
    Console.WriteLine("Abrígate");
}

string estado = temperatura > 25 ? "verano" : "no verano";
Console.WriteLine(estado);
salidaHace calor
verano

La última línea usa el operador condicional ?:, un if de una línea que devuelve un valor: si la condición se cumple da lo de antes de los dos puntos, si no, lo de después.

El error que te ahorra: escribir = donde iba ==

int intentos = 2;

if (intentos = 3)
{
    Console.WriteLine("Último intento");
}
dotnet buildif_asigna.cs(3,5): error CS0029: No se puede convertir implícitamente el tipo 'int' en 'bool'

En C y en JavaScript, if (intentos = 3) asigna 3 y la condición se cumple siempre: un fallo clásico que puede pasar meses sin que nadie lo vea. En C# la asignación da un int, y un int no es un bool, así que no compila. Lo que querías escribir era intentos == 3.

switch: elegir entre muchos casos

Cuando hay muchos casos, el switch es más claro que una cadena de else if. C# tiene dos versiones. La clásica es una instrucción: cada caso ejecuta código y termina con break.

string dia = "sábado";

switch (dia)
{
    case "sábado":
    case "domingo":
        Console.WriteLine("Fin de semana");
        break;
    case "lunes":
        Console.WriteLine("Vuelta al trabajo");
        break;
    default:
        Console.WriteLine("Entre semana");
        break;
}
salidaFin de semana

A diferencia de C, en C# no se puede «caer» de un caso con código al siguiente: si olvidas el break, el compilador lo marca como error. Dos case seguidos sin nada entre ellos, como sábado y domingo, sí están permitidos.

La versión moderna es una expresión switch, que devuelve un valor. Es más corta y, sobre todo, admite patrones: no solo valores exactos, también comparaciones, rangos combinados con and y or, y tipos.

string Clasificar(int edad) => edad switch
{
    < 0 => "edad no válida",
    < 13 => "niño",
    >= 13 and < 18 => "adolescente",
    65 or > 65 => "jubilado",
    _ => "adulto"
};

foreach (int edad in new[] { 8, 15, 40, 70, -1 })
{
    Console.WriteLine($"{edad}: {Clasificar(edad)}");
}
salida8: niño
15: adolescente
40: adulto
70: jubilado
-1: edad no válida

Los casos se prueban en orden y gana el primero que encaja; el _ es el «cualquier otra cosa». La flecha => de la primera línea define una función de una sola expresión: las funciones tienen su propia lección, la 4.

Si un switch de expresión no cubre todos los casos posibles, el compilador avisa. Y si al ejecutar llega un valor que no encaja en ninguno, el programa falla:

string Nombre(int dia) => dia switch
{
    1 => "lunes",
    2 => "martes",
    3 => "miércoles"
};

Console.WriteLine(Nombre(2));
Console.WriteLine(Nombre(7));
dotnet buildswitch_incompleto.cs(1,31): warning CS8509: La expresión switch no controla todos los valores posibles de su tipo de entrada (no es exhaustivo). Por ejemplo, el patrón "0" no está incluido.
salida con errormartes
Unhandled exception. System.Runtime.CompilerServices.SwitchExpressionException: Non-exhaustive switch expression failed to match its input.

El aviso CS8509 incluso propone un valor concreto que se queda fuera. Añadir el caso _ lo resuelve.

Repetir: for, foreach, while

for (int i = 1; i <= 3; i++)
{
    Console.Write($"{i} ");
}
Console.WriteLine();

string[] frutas = ["pera", "uva", "kiwi"];
foreach (string fruta in frutas)
{
    Console.Write($"{fruta} ");
}
Console.WriteLine();

int n = 100;
while (n > 10)
{
    n /= 2;
}
Console.WriteLine(n);

int intentos = 0;
do
{
    intentos++;
} while (intentos < 3);
Console.WriteLine(intentos);
salida1 2 3 
pera uva kiwi 
6
3

for es para contar; foreach, para recorrer los elementos de una colección sin preocuparte por los índices, y es el que más usarás; while repite mientras se cumpla la condición, y do ... while lo mismo pero comprobándola al final, así que el bloque se ejecuta al menos una vez. Dentro de cualquiera, break sale del bucle y continue salta a la siguiente vuelta.

Arrays

Un array guarda un número fijo de elementos del mismo tipo. Se crea con un tamaño o con sus valores, y se accede por posición empezando en 0. Desde C# 12, la forma más corta de escribir los valores es la expresión de colección, con corchetes:

int[] notas = new int[3];
notas[0] = 7;
Console.WriteLine(string.Join(", ", notas));

string[] letras = ["A", "B", "C", "D", "E"];
Console.WriteLine(letras.Length);
Console.WriteLine(letras[0]);
Console.WriteLine(letras[^1]);
Console.WriteLine(string.Join(", ", letras[1..3]));
Console.WriteLine(string.Join(", ", letras[^2..]));
salida7, 0, 0
5
A
E
B, C
D, E

Un array nuevo se rellena con el valor por omisión de su tipo: ceros para los números, false para bool y null para las referencias. Las tres últimas líneas usan dos novedades relativamente recientes del lenguaje: ^1 cuenta desde el final, y .. toma un tramo.

Un array de cinco letras de la A a la E. Arriba, sus índices del 0 al 4; abajo, los índices desde el final, de ^5 a ^1. El rango letras[1..3] toma B y C: incluye el índice 1 y excluye el 3Índices desde el principio y desde el finalíndiceletrasdesde el final[0]A[^5][1]B[^4][2]C[^3][3]D[^2][4]E[^1]letras[1..3] da B, CEl rango incluye el primero y excluye el último. ^1 es el último elemento.

Cada elemento tiene dos índices: el normal, desde el principio, y el que empieza por ^, desde el final. Un rango incluye su primer índice y excluye el último. Esquema: decodigo.com.

Salirse del array no devuelve undefined como en JavaScript: lanza una excepción.

int[] datos = [10, 20, 30];
Console.WriteLine(datos[3]);
salida con errorUnhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.

List: una lista que crece

El tamaño de un array no cambia. Cuando no sabes cuántos elementos vas a tener, usa List<T>, donde T es el tipo de los elementos: List<string> es una lista de textos. Es la colección más usada de .NET.

List<string> tareas = ["comprar pan", "llamar a Ana"];
tareas.Add("pagar la luz");
tareas.Insert(0, "desayunar");
tareas.Remove("llamar a Ana");

Console.WriteLine(tareas.Count);
Console.WriteLine(tareas.Contains("pagar la luz"));
Console.WriteLine(tareas.IndexOf("comprar pan"));

for (int i = 0; i < tareas.Count; i++)
{
    Console.WriteLine($"{i + 1}. {tareas[i]}");
}
salida3
True
1
1. desayunar
2. comprar pan
3. pagar la luz

Fíjate en que los arrays tienen Length y las listas Count: un pequeño despiste que todo el mundo comete al principio, y que el compilador detecta. Una regla importante: no se puede cambiar una lista mientras se recorre con foreach.

List<int> numeros = [1, 2, 3, 4];

foreach (int n in numeros)
{
    if (n % 2 == 0)
    {
        numeros.Remove(n);
    }
}
salida con errorUnhandled exception. System.InvalidOperationException: Collection was modified; enumeration operation may not execute.

Para quitar elementos según una condición está RemoveAll, que lo hace de una vez: numeros.RemoveAll(n => n % 2 == 0). Lo que va dentro del paréntesis es una lambda, que veremos en la lección 4.

Dictionary: buscar por clave

Un Dictionary<TClave, TValor> guarda parejas de clave y valor, y encuentra el valor de una clave al instante, tenga diez elementos o diez millones. Es lo que en otros lenguajes se llama mapa, hash o diccionario.

var capitales = new Dictionary<string, string>
{
    ["Perú"] = "Lima",
    ["Chile"] = "Santiago",
};
capitales["México"] = "Ciudad de México";
capitales["Chile"] = "Santiago de Chile";

Console.WriteLine(capitales["Perú"]);
Console.WriteLine(capitales.Count);
Console.WriteLine(capitales.ContainsKey("España"));

if (capitales.TryGetValue("Colombia", out string? capital))
{
    Console.WriteLine(capital);
}
else
{
    Console.WriteLine("No tengo la capital de Colombia");
}

foreach (var (pais, ciudad) in capitales)
{
    Console.WriteLine($"{pais}: {ciudad}");
}
salidaLima
3
False
No tengo la capital de Colombia
Perú: Lima
Chile: Santiago de Chile
México: Ciudad de México

Asignar con corchetes añade la clave o, si ya existía, sustituye su valor: por eso Chile aparece una sola vez. Leer una clave que no existe, en cambio, lanza una excepción (KeyNotFoundException), así que cuando no estés seguro usa TryGetValue, que sigue el mismo patrón que int.TryParse de la lección 2. El foreach del final separa cada pareja en dos variables a la vez.

Un uso típico es contar cuántas veces aparece cada cosa:

string texto = "el perro y el gato y el loro";
var cuenta = new Dictionary<string, int>();

foreach (string palabra in texto.Split(' '))
{
    cuenta[palabra] = cuenta.GetValueOrDefault(palabra) + 1;
}

foreach (var (palabra, veces) in cuenta)
{
    Console.WriteLine($"{palabra}: {veces}");
}
salidael: 3
perro: 1
y: 2
gato: 1
loro: 1

HashSet: elementos sin repetir

Si solo te importa si algo está o no, sin valor asociado ni orden, HashSet<T> guarda cada elemento una sola vez y responde a Contains igual de rápido que un diccionario:

var vistos = new HashSet<string>();
string[] visitas = ["ana", "luis", "ana", "eva", "luis"];

foreach (string usuario in visitas)
{
    if (!vistos.Add(usuario))
    {
        Console.WriteLine($"{usuario} ya había entrado");
    }
}
Console.WriteLine($"Usuarios distintos: {vistos.Count}");
salidaana ya había entrado
luis ya había entrado
Usuarios distintos: 3

Add devuelve false cuando el elemento ya estaba, lo que permite detectar repetidos en una sola línea.

Resumen

NecesitasUsaRecuerda
Elegir entre dos caminosif / else, o c ? a : bla condición tiene que ser bool
Elegir entre muchos valoresexpresión switch con patronesañade el caso _
Recorrer una colecciónforeachno la modifiques dentro
Un número fijo de elementosint[] a = [1, 2, 3];Length; fuera de rango, excepción
El último, un tramoa[^1], a[1..3]el rango excluye el final
Una lista que creceList<T>Count, Add, Remove
Buscar por claveDictionary<K, V>TryGetValue si puede faltar
Sin repetidosHashSet<T>Add devuelve si era nuevo

Algunos comportamientos de las colecciones sorprenden incluso a quien ya programa en C#, como lo que pasa al guardar un struct en una lista. Están en las rarezas de C#. Y para ordenar y buscar en listas de verdad, mira el ejemplo ordenar y buscar en C#.

Retos

Reto 1. Escribe una expresión switch que convierta una nota numérica del 0 al 10 en texto: menos de 5, «suspenso»; de 5 a menos de 7, «aprobado»; de 7 a menos de 9, «notable»; 9 o más, «sobresaliente». Cualquier valor fuera de 0 a 10 debe dar «nota no válida».

Ver la solución
string Calificar(double nota) => nota switch
{
    < 0 or > 10 => "nota no válida",
    < 5 => "suspenso",
    < 7 => "aprobado",
    < 9 => "notable",
    _ => "sobresaliente"
};

foreach (double nota in new[] { 4.9, 5, 8.5, 10, 11 })
{
    Console.WriteLine($"{nota}: {Calificar(nota)}");
}
salida4.9: suspenso
5: aprobado
8.5: notable
10: sobresaliente
11: nota no válida

Como los casos se prueban en orden, basta con poner el límite superior de cada tramo: cuando se llega a < 7, ya se sabe que la nota no es menor que 5. El caso no válido va primero para que no lo capture otro.

Reto 2. Dada la lista [3, 8, 1, 9, 4], escribe con un foreach el mayor de sus números y su posición, sin usar ningún método de la lista que lo calcule por ti.

Ver la solución
List<int> numeros = [3, 8, 1, 9, 4];
int mayor = numeros[0];
int posicion = 0;
int i = 0;

foreach (int n in numeros)
{
    if (n > mayor)
    {
        mayor = n;
        posicion = i;
    }
    i++;
}

Console.WriteLine($"El mayor es {mayor}, en la posición {posicion}");
salidaEl mayor es 9, en la posición 3

Se empieza suponiendo que el mayor es el primero y se corrige al encontrar uno más grande. Con un for no harían falta dos variables, porque el índice ya lo da el bucle.

Reto 3. Con un diccionario, agrupa estas palabras por su primera letra y escribe cada letra con sus palabras: ["sol", "mar", "sal", "luna", "mesa"].

Ver la solución
string[] palabras = ["sol", "mar", "sal", "luna", "mesa"];
var grupos = new Dictionary<char, List<string>>();

foreach (string palabra in palabras)
{
    char letra = palabra[0];
    if (!grupos.TryGetValue(letra, out List<string>? lista))
    {
        lista = [];
        grupos[letra] = lista;
    }
    lista.Add(palabra);
}

foreach (var (letra, lista) in grupos)
{
    Console.WriteLine($"{letra}: {string.Join(", ", lista)}");
}
salidas: sol, sal
m: mar, mesa
l: luna

El valor del diccionario es otra colección. Como la lista es un tipo de referencia, la que se guarda en el diccionario y la de la variable lista son la misma: añadir a una es añadir a la otra. En la lección 7 verás que LINQ lo hace en una línea con GroupBy.

Todo el código de esta lección se compiló con el .NET SDK 10.0.403 (C# 14) y se ejecutó con .NET 10.0.14 en un contenedor limpio antes de publicarla; los mensajes del compilador y las salidas están copiados de esa ejecución. ¿Algo no te funciona? Revisa que tengas .NET 10: dotnet --version.