Ir al contenido
wannacode

Lanzar excepciones y crear las tuyas

En resumen: Con throw un método se niega y lanza una excepción que cae a través de las llamadas hasta que algún catch la atrapa; para tu propio caso escribes tu propia clase de excepción.

Problema

El cajero dice «recoge tu dinero»

La cuenta guarda su saldo. Si el importe a retirar es negativo o no hay dinero suficiente, el método simplemente no hace nada:

public void Withdraw(int amount)
{
if (amount > 0 && amount <= Balance)
{
Balance -= amount;
}
}

El cajero lo usa así:

Account account = new Account(100);
account.Withdraw(500);
Console.WriteLine("Recoge tu dinero.");
Console.WriteLine("Saldo: " + account.Balance + " €");
Salida
Recoge tu dinero.
Saldo: 100 €

El cliente pide 500 € y tiene 100 €. El método se niega en silencio, el cajero no se da cuenta y dice «Recoge tu dinero.». El error se pierde. El método podría devolver true o false, pero quien lo llama puede olvidarse fácilmente de comprobarlo, y false no dice por qué.

La idea

throw: me niego y digo por qué

En lugar de callarse, el método lanza una excepción:

public void Withdraw(int amount)
{
if (amount <= 0)
{
throw new ArgumentException("El importe debe ser positivo");
}
if (amount > Balance)
{
throw new ArgumentException("No alcanza el dinero");
}
Balance -= amount;
}

throw termina el método como return. A la línea Balance -= amount nunca se llega. Pero en lugar de un valor, el método envía una excepción, y quien lo llama no puede pasarla por alto:

Account account = new Account(100);
try
{
account.Withdraw(500);
Console.WriteLine("Recoge tu dinero.");
}
catch (ArgumentException e)
{
Console.WriteLine("Rechazado: " + e.Message);
}
Console.WriteLine("Saldo: " + account.Balance + " €");
Salida
Rechazado: No alcanza el dinero
Saldo: 100 €
  • new crea el objeto de la excepción, como cualquier objeto. El mensaje va a su constructor.
  • ArgumentException es la excepción ya hecha para «el argumento es incorrecto». La usas justo en este tipo de comprobación.
Más precisamente, para curiosos
  • ArgumentOutOfRangeException es para un número fuera de los límites permitidos, e InvalidOperationException es para un objeto que no está en un estado que permita esta acción, como retirar de una cuenta cerrada.
  • throw funciona solo con un objeto de una clase hija de Exception.

Míralo

La excepción cae

public void Withdraw(int amount)
{
if (amount > Balance)
{
throw new ArgumentException("No alcanza el dinero");
}
Balance -= amount;
}
static void PayRent(Account account)
{
account.Withdraw(800);
Console.WriteLine("El alquiler está pagado.");
}
try
{
PayRent(new Account(500));
}
catch (ArgumentException e)
{
Console.WriteLine("Rechazado: " + e.Message);
}
Paso a paso, en palabras
  1. Main llama a PayRent para una cuenta con 500 €.
  2. PayRent llama a Withdraw(800): un marco nuevo encima.
  3. 800 > 500: el método lanza una excepción.
  4. El saldo no cambia: el marco se descarta sin terminar sus líneas.
  5. PayRent no tiene catch: «El alquiler está pagado.» no se imprime, el marco se descarta.
  6. catch en Main atrapa la excepción y el programa sigue desde aquí.

Recuerda la pila de llamadas de la lección de recursión. Main llama a PayRent, y este llama a Withdraw. Los marcos están uno encima de otro.

Cuando Withdraw lanza, la excepción busca un catch bajando por la pila. Un marco sin un catch que coincida se descarta enseguida, sin terminar sus líneas. Eso es lo que le pasa también a PayRent. Solo Main tiene un catch, y el programa sigue desde ahí.

Si Main tampoco tuviera catch, también se descartaría. Entonces el programa se detiene e imprime un stack trace: justo estos marcos, de dentro hacia fuera.

Pruébalo

¿Qué llega a la pantalla?

La cuenta tiene 50 €, y el pedido es de 120 €.

¿Qué imprimirá el programa?

static void Checkout(Account account)
{
Console.WriteLine("Pago el pedido");
account.Withdraw(120);
Console.WriteLine("El pedido está pagado");
}
Account account = new Account(50);
try
{
Checkout(account);
Console.WriteLine("Envío la confirmación");
}
catch (ArgumentException e)
{
Console.WriteLine("Rechazado: " + e.Message);
}
Console.WriteLine("Saldo: " + account.Balance + " €");
Respuesta

Pruébalo

Encuentra el error

El alquiler es de 800 €, y la cuenta tiene 500 €. El programa dice «El alquiler está pagado.». ¿En qué línea está el error?

Elige la línea con el error.

Más precisamente, para curiosos
  • A veces un método atrapa, hace algo, como escribir en un registro, y vuelve a lanzar la misma excepción: throw; en catch. Solo throw;, sin nombre, conserva el stack trace original. throw e; lo empieza de nuevo desde esta línea.
  • También puedes lanzar otra excepción que diga mejor qué pasó. Entonces pasa la antigua como segundo argumento del constructor: new InvalidOperationException("El alquiler no está pagado", e). Verás las dos en el stack trace.

La idea

Tu propia excepción

«No alcanza el dinero» es texto. Si el cajero quiere proponer «Ingresa 30 € más», tiene que saber cuánto falta. Sacar el número del texto es frágil. Mejor haz tu propia clase de excepción con un campo:

class InsufficientFundsException : Exception
{
public int Shortfall { get; }
public InsufficientFundsException(int shortfall) : base("Faltan " + shortfall + " €")
{
Shortfall = shortfall;
}
}

Tu propia excepción es una clase normal que hereda de Exception. El constructor pasa el mensaje a la clase padre con : base(...). El método la lanza cuando no alcanza el dinero:

public void Withdraw(int amount)
{
if (amount <= 0)
{
throw new ArgumentException("El importe debe ser positivo");
}
if (amount > Balance)
{
throw new InsufficientFundsException(amount - Balance);
}
Balance -= amount;
}

Y quien lo llama atrapa los dos casos por separado:

Account account = new Account(100);
try
{
account.Withdraw(130);
}
catch (InsufficientFundsException e)
{
Console.WriteLine(e.Message);
Console.WriteLine("Ingresa " + e.Shortfall + " € más o elige un importe menor.");
}
catch (ArgumentException e)
{
Console.WriteLine("Rechazado: " + e.Message);
}
Salida
Faltan 30 €
Ingresa 30 € más o elige un importe menor.
Más precisamente, para curiosos
  • Los nombres de las excepciones siempre terminan en Exception.
  • Tu propia excepción también puede heredar de una clase ya hecha más concreta, como ArgumentException. Entonces un catch para la clase padre también la atrapa. Aquí es aparte, así que el orden de los dos bloques catch no importa.

La idea

¿Propia o ya hecha?

En C# tu propia excepción hereda de Exception, y ya está: no hay excepciones comprobadas. La pregunta es otra: ¿necesitas una propia?

Una excepción ya hecha, cuando el significado coincide: ArgumentException para un argumento incorrecto, InvalidOperationException para una acción en el momento equivocado. Una propia, cuando quien llama tiene que distinguirla de las demás y reaccionar a su manera, o cuando la excepción tiene que llevar datos, como shortfall.

Pruébalo

Rellena el hueco

Una excepción para una edad no válida. ¿Qué va en el hueco para que el mensaje llegue a la clase padre?

class InvalidAgeException : Exception
{
    public InvalidAgeException(string message) : (message)
    {
    }
}

Ejercicio

Excepciones en clases

Resuélvelas en tu editor.

1. Tienda de música. La tienda toca instrumentos en una demostración, pero no todos se pueden tocar.

  • Una clase abstracta MusicalInstrument con un nombre, una marca y un precio (propiedades de solo lectura), y un constructor.
  • Una interfaz IPlayable con un método Play().
  • Guitar y Piano heredan del instrumento y pueden tocar. Drum solo hereda.
  • NotPlayableException : Exception.
  • MusicShop con un método DemoInstrument(MusicalInstrument instrument): si el instrumento puede tocar (is IPlayable playable), lo toca; si no, lanza NotPlayableException.
  • En Main, haz la demostración de una guitarra Fender Stratocaster (1299 €), una batería Pearl Export (849 €) y un piano Yamaha P-45 (499 €) desde un array. Atrapa la excepción de cada instrumento por separado, para que la demostración siga.
Salida
Fender Stratocaster, 1299.00 €
La guitarra toca un acorde.
Pearl Export, 849.00 €
Pearl Export no se puede usar en una demostración.
Yamaha P-45, 499.00 €
El piano toca una escala.

2. Una cuenta con reglas. Añade a Account un método Deposit(int amount) que lance ArgumentException con un importe de 0 o menos. Retirar lanza InsufficientFundsException de la lección. La cuenta empieza con 100 €. Lee el número de operaciones y después las propias operaciones, cada una en su propia línea: «ingresa 50» o «retira 500». Después de cada una, imprime el saldo o por qué se rechazó la operación.

Entrada
4
ingresa 50
retira 500
retira -5
retira 120
Salida
Saldo: 150 €
Faltan 350 €. Saldo: 150 €
El importe debe ser positivo. Saldo: 150 €
Saldo: 30 €

3. Edad no válida. Person tiene un nombre y una edad. La propiedad Age lanza InvalidAgeException de la tarjeta anterior si la edad es menor que 0 o mayor que 150. El constructor también pasa por Age, para que nadie pueda crear una persona con una edad no válida. Crea a María (19), Pablo (−3) y Juan (200).

Salida
María, 19 años
Pablo: Edad no válida: -3
Juan: Edad no válida: 200

Más tareas

  • Reserva de mesa. Una clase Restaurant con un número de mesas libres y un método para reservar una. Si se han acabado las mesas, lanza tu propia NoTablesException. Si el número de invitados es 0 o menos, lanza la excepción ya hecha para un argumento incorrecto.
  • Un producto con existencias. Una clase Product con una cantidad en existencias y un método Sell(int qty). Si no hay suficiente en existencias, lanza OutOfStockException, que lleva cuántas unidades hay. La tienda atrapa la excepción y ofrece vender las que hay en existencias.
  • Tienda de música con motivo. Añade a NotPlayableException un campo con el instrumento que no puede tocar, e imprime su precio en catch.

Test

Ponte a prueba

1. ¿Qué hace throw new ArgumentException("...")?
2. main llama a a, a llama a b, b lanza una excepción. Solo main tiene un catch. ¿Qué pasa con las líneas de a después de la llamada a b?
3. ¿Dónde es mejor atrapar la excepción?
4. ¿De qué clase hereda tu propia excepción en C#?

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