Lección 4 de 8. Cuando un programa crece, hay que partirlo en trozos con nombre, los métodos, y decidir qué pasa cuando algo sale mal. Esta lección va de las dos cosas: cómo se declaran y se llaman los métodos, las funciones como valores (lambdas), y el sistema de excepciones de .NET con try, catch, finally y using.
Métodos: dar nombre a un trozo de código
Un método agrupa instrucciones bajo un nombre, recibe datos por sus parámetros y puede devolver un resultado. Delante del nombre va el tipo que devuelve, o void si no devuelve nada. Con instrucciones de nivel superior se pueden escribir directamente en el archivo, como funciones locales; dentro de una clase son exactamente iguales, como verás en la lección 5.
double AreaCirculo(double radio)
{
return Math.PI * radio * radio;
}
void Saludar(string nombre)
{
Console.WriteLine($"Hola, {nombre}");
}
int Doble(int n) => n * 2;
Saludar("Eva");
Console.WriteLine(AreaCirculo(2).ToString("F2"));
Console.WriteLine(Doble(21));salidaHola, Eva 12.57 42
El tercero usa la forma de expresión, con =>: cuando el método es una sola expresión, se ahorran las llaves y el return. Por convención, los nombres de los métodos van en PascalCase, con la primera letra en mayúscula, y los de las variables y parámetros en camelCase.
El error que te ahorra: olvidar devolver un valor en algún camino
string Signo(int n)
{
if (n > 0)
{
return "positivo";
}
else if (n < 0)
{
return "negativo";
}
}
Console.WriteLine(Signo(0));dotnet buildsin_return.cs(1,8): error CS0161: 'Signo(int)': no todas las rutas de acceso de código devuelven un valor
El método dice que devuelve un string, pero si n es 0 no llega a ningún return. En JavaScript devolvería undefined sin avisar y el fallo aparecería más adelante, lejos de su causa. El compilador de C# recorre todos los caminos posibles y se niega a compilar hasta que añadas el caso que falta.
Parámetros opcionales y con nombre
Un parámetro con un valor por omisión es opcional: si no se pasa, toma ese valor. Y al llamar a un método se puede poner el nombre del parámetro delante, lo que permite saltarse opcionales y deja más claro qué significa cada argumento:
string Formatear(decimal importe, string moneda = "USD", int decimales = 2)
{
return $"{Math.Round(importe, decimales)} {moneda}";
}
Console.WriteLine(Formatear(1299.987m));
Console.WriteLine(Formatear(1299.987m, "EUR"));
Console.WriteLine(Formatear(1299.987m, decimales: 0));
Console.WriteLine(Formatear(moneda: "MXN", importe: 50m));salida1299.99 USD 1299.99 EUR 1300 USD 50 MXN
Los opcionales van siempre al final de la lista. Con params, el último parámetro acepta cualquier número de argumentos, que llegan como una colección:
int Sumar(params int[] numeros)
{
int total = 0;
foreach (int n in numeros)
{
total += n;
}
return total;
}
Console.WriteLine(Sumar());
Console.WriteLine(Sumar(4));
Console.WriteLine(Sumar(1, 2, 3, 4));salida0 4 10
Por valor, ref y out
Al llamar a un método, C# copia cada argumento en su parámetro. Con un tipo de valor, como int, se copia el número y el método no puede cambiar la variable de quien llama; con un tipo de referencia se copia la referencia, así que el método sí puede cambiar el objeto, igual que viste en la lección 2. Si de verdad quieres que el método cambie la variable original, se pasa con ref:
void IntentarCambiar(int n)
{
n = 100;
}
void Cambiar(ref int n)
{
n = 100;
}
void Anadir(List<string> lista)
{
lista.Add("nuevo");
}
int x = 1;
IntentarCambiar(x);
Console.WriteLine(x);
Cambiar(ref x);
Console.WriteLine(x);
List<string> nombres = ["Ana"];
Anadir(nombres);
Console.WriteLine(string.Join(", ", nombres));salida1 100 Ana, nuevo
out es parecido, pero para resultados: el método está obligado a darle un valor antes de terminar, y quien llama puede declarar la variable en la propia llamada. Es lo que hace int.TryParse. Aun así, si un método tiene que devolver varias cosas, hoy es más cómodo devolver una tupla:
(int minimo, int maximo) Extremos(List<int> datos)
{
int min = datos[0], max = datos[0];
foreach (int d in datos)
{
if (d < min) min = d;
if (d > max) max = d;
}
return (min, max);
}
var r = Extremos([7, 2, 9, 4]);
Console.WriteLine($"{r.minimo} y {r.maximo}");
var (menor, mayor) = Extremos([15, 30, 5]);
Console.WriteLine($"{menor} y {mayor}");salida2 y 9 5 y 30
Lambdas: funciones como valores
Una lambda es una función sin nombre que se escribe en el sitio donde se usa: n => n * 2 se lee «dado n, devuelve n por 2». Se puede guardar en una variable, pasar a otro método o devolver. Sus tipos son Func, si devuelve algo (el último tipo es el del resultado), y Action, si no devuelve nada:
Func<int, int> doble = n => n * 2;
Func<int, int, int> sumar = (a, b) => a + b;
Action<string> gritar = texto => Console.WriteLine(texto.ToUpper() + "!");
Console.WriteLine(doble(8));
Console.WriteLine(sumar(3, 4));
gritar("cuidado");
List<int> AplicarATodos(List<int> datos, Func<int, int> f)
{
var resultado = new List<int>();
foreach (int d in datos)
{
resultado.Add(f(d));
}
return resultado;
}
Console.WriteLine(string.Join(", ", AplicarATodos([1, 2, 3], doble)));
Console.WriteLine(string.Join(", ", AplicarATodos([1, 2, 3], n => n * n)));salida16 7 CUIDADO! 2, 4, 6 1, 4, 9
Pasar funciones a otros métodos es la base de LINQ, la forma habitual de filtrar, transformar y ordenar colecciones en C#, que verás en la lección 7. Por ejemplo, el RemoveAll de la lección anterior recibe una lambda que dice qué elementos quitar.
Excepciones: cuando algo sale mal
Ya has visto varias: dividir un entero entre cero, salirse de un array o leer una clave que no existe lanzan una excepción. Si nadie la captura, el programa termina con el mensaje de error. Para capturarla se usa try y catch:
int Dividir(int a, int b) => a / b;
try
{
Console.WriteLine(Dividir(10, 2));
Console.WriteLine(Dividir(10, 0));
Console.WriteLine("Esta línea no llega a ejecutarse");
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
finally
{
Console.WriteLine("Fin del cálculo");
}
Console.WriteLine("El programa sigue");salida5 Error: Attempted to divide by zero. Fin del cálculo El programa sigue
El catch indica qué tipo de excepción captura, y se pueden poner varios, del más concreto al más general: el primero que encaja es el que se ejecuta. Por eso el compilador no deja poner uno general antes que uno concreto, porque el segundo no se ejecutaría nunca:
try
{
int.Parse("abc");
}
catch (Exception)
{
Console.WriteLine("Algo falló");
}
catch (FormatException)
{
Console.WriteLine("No es un número");
}dotnet buildcatch_orden.cs(9,8): error CS0160: Una cláusula catch previa ya detecta todas las excepciones de este tipo o de tipo superior ('Exception')Con when se añade una condición: el catch solo actúa si se cumple. Y con throw se lanza una excepción propia, normalmente para rechazar datos que no tienen sentido:
decimal Retirar(decimal saldo, decimal importe)
{
if (importe <= 0)
{
throw new ArgumentOutOfRangeException(nameof(importe), "El importe debe ser positivo");
}
if (importe > saldo)
{
throw new InvalidOperationException($"Saldo insuficiente: tienes {saldo} y pides {importe}");
}
return saldo - importe;
}
foreach (decimal importe in new[] { 30m, 500m, -5m })
{
try
{
Console.WriteLine($"Te quedan {Retirar(100m, importe)}");
}
catch (InvalidOperationException ex) when (ex.Message.Contains("insuficiente"))
{
Console.WriteLine($"Rechazado. {ex.Message}");
}
catch (ArgumentException ex)
{
Console.WriteLine($"Dato incorrecto: {ex.Message}");
}
}salidaTe quedan 70 Rechazado. Saldo insuficiente: tienes 100 y pides 500 Dato incorrecto: El importe debe ser positivo (Parameter 'importe')
El tercer caso cae en el catch de ArgumentException porque ArgumentOutOfRangeException es un tipo particular de ella: un catch captura su tipo y todos los que derivan de él. nameof(importe) escribe el nombre del parámetro como texto, y si un día lo renombras, se actualiza solo.
Tus propias excepciones
Cuando un error es propio de tu programa, crear un tipo de excepción permite capturarlo sin confundirlo con otros. Basta una clase que herede de Exception; la herencia la explicamos en la lección 6:
void Reservar(int asientosLibres, int pedidos)
{
if (pedidos > asientosLibres)
{
throw new SinPlazasException(pedidos - asientosLibres);
}
Console.WriteLine($"Reservados {pedidos} asientos");
}
try
{
Reservar(10, 4);
Reservar(10, 12);
}
catch (SinPlazasException ex)
{
Console.WriteLine($"{ex.Message} Faltan {ex.Faltan}.");
}
class SinPlazasException(int faltan) : Exception("No hay plazas suficientes.")
{
public int Faltan { get; } = faltan;
}salidaReservados 4 asientos No hay plazas suficientes. Faltan 2.
Cuándo no usar excepciones
Las excepciones son para lo excepcional: un archivo que desaparece, una red que se cae, un error de programación. No para el flujo normal. Que un usuario escriba letras donde iba un número es de lo más normal, y para eso existen los métodos TryParse y TryGetValue, que devuelven true o false en vez de lanzar. Además, lanzar y capturar una excepción es mucho más lento que un if.
using: liberar lo que se abre
Algunos objetos tienen recursos que hay que devolver al terminar: un archivo abierto, una conexión a una base de datos. Implementan la interfaz IDisposable, con un método Dispose que los libera. La forma segura de usarlos es using, que llama a Dispose al salir del bloque aunque haya una excepción, como si fuera un finally automático:
string ruta = Path.Combine(Path.GetTempPath(), "notas.txt");
using (var escritor = new StreamWriter(ruta))
{
escritor.WriteLine("primera línea");
escritor.WriteLine("segunda línea");
}
using var lector = new StreamReader(ruta);
Console.WriteLine(lector.ReadToEnd());salidaprimera línea segunda línea
La segunda forma, sin llaves (la declaración using), libera el objeto al final del bloque en el que está, aquí al terminar el programa. Para entender el orden exacto, nada mejor que un objeto que avisa cuando se libera:
try
{
using (var a = new Recurso("A"))
{
using var b = new Recurso("B");
Console.WriteLine("Trabajando");
throw new Exception("algo falló");
}
}
catch (Exception ex)
{
Console.WriteLine($"Capturada: {ex.Message}");
}
class Recurso : IDisposable
{
private readonly string nombre;
public Recurso(string nombre)
{
this.nombre = nombre;
Console.WriteLine($"Abro {nombre}");
}
public void Dispose() => Console.WriteLine($"Cierro {nombre}");
}salidaAbro A Abro B Trabajando Cierro B Cierro A Capturada: algo falló
Los recursos se cierran en orden inverso al de apertura, y antes de que el catch reciba la excepción: cuando llega, el archivo o la conexión ya están liberados. Lo verás en ejemplos como leer un archivo de texto o usar SQLite.
Resumen
| Para | Escribe | Recuerda |
|---|---|---|
| Un método | int Doble(int n) => n * 2; | todo camino debe devolver un valor |
| Parámetro opcional | string moneda = "USD" | al final; se puede llamar por nombre |
| Cambiar la variable de fuera | ref (entra y sale), out (solo sale) | mejor devolver una tupla |
| Varios resultados | (int min, int max) | var (a, b) = ... los separa |
| Una función como valor | Func<int, int> f = n => n * 2; | Action si no devuelve nada |
| Capturar un error | try / catch (Tipo ex) | del más concreto al más general |
| Código que siempre se ejecuta | finally | o using para liberar recursos |
| Rechazar un dato | throw new ArgumentException(...) | no para el flujo normal: TryParse |
Retos
Reto 1. Escribe un método Precio que reciba un importe, un porcentaje de descuento opcional (0 por omisión) y un IVA opcional (21 por omisión), y devuelva el precio final. Llámalo una vez con solo el importe y otra indicando únicamente el IVA, con su nombre.
Ver la solución
decimal Precio(decimal importe, decimal descuento = 0, decimal iva = 21)
{
decimal conDescuento = importe * (1 - descuento / 100);
return Math.Round(conDescuento * (1 + iva / 100), 2);
}
Console.WriteLine(Precio(100m));
Console.WriteLine(Precio(100m, iva: 16));salida121.00 116.00
El argumento con nombre permite saltarse descuento y dar solo iva. Para dinero, decimal, como se vio en la lección 2.
Reto 2. Escribe un método Filtrar que reciba una lista de enteros y una condición como Func<int, bool>, y devuelva una lista nueva con los que la cumplen. Úsalo para sacar los pares y los mayores de 10 de [3, 12, 7, 20, 8].
Ver la solución
List<int> Filtrar(List<int> datos, Func<int, bool> condicion)
{
var resultado = new List<int>();
foreach (int d in datos)
{
if (condicion(d))
{
resultado.Add(d);
}
}
return resultado;
}
List<int> numeros = [3, 12, 7, 20, 8];
Console.WriteLine(string.Join(", ", Filtrar(numeros, n => n % 2 == 0)));
Console.WriteLine(string.Join(", ", Filtrar(numeros, n => n > 10)));salida12, 20, 8 12, 20
Acabas de escribir a mano el Where de LINQ: numeros.Where(n => n % 2 == 0) hace lo mismo, como verás en la lección 7.
Reto 3. Lee números de una lista de textos ["10", "x", "25", "", "7"] y súmalos, ignorando los que no son números. Hazlo sin try/catch, y escribe cuántos se ignoraron.
Ver la solución
string[] entradas = ["10", "x", "25", "", "7"];
int suma = 0, ignorados = 0;
foreach (string e in entradas)
{
if (int.TryParse(e, out int n))
{
suma += n;
}
else
{
ignorados++;
}
}
Console.WriteLine($"Suma: {suma}, ignorados: {ignorados}");salidaSuma: 42, ignorados: 2
Un texto que no es un número es algo esperable en datos que escribe una persona, así que no merece una excepción: TryParse es la herramienta.
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.
