Ir al contenido
wannacode

try, catch y finally

En resumen: Con varios bloques catch atrapas tipos distintos de excepciones, de lo concreto a lo general, y finally se ejecuta siempre, con error o sin él.

Problema

La cuenta para cero personas

El programa divide la cuenta del restaurante a partes iguales. Ya conoces la entrada incorrecta, y la atrapas:

Program.cs
class Program
{
static void Main()
{
try
{
int bill = int.Parse(Console.ReadLine());
int people = int.Parse(Console.ReadLine());
Console.WriteLine("Por persona: " + bill / people + " €");
}
catch (FormatException)
{
Console.WriteLine("Escribe números enteros.");
}
}
}

La cuenta es de 90 €, y alguien escribe 0 como número de personas. El programa vuelve a detenerse:

Error
Unhandled exception. System.DivideByZeroException: Attempted to divide by zero.
at Program.Main() in /projects/Bill/Program.cs:line 9

catch está ahí, pero no ayuda. Solo atrapa FormatException, y esta es otra clase de excepción.

La idea

Varios bloques catch

Después de un try pueden ir varios bloques catch, uno para cada tipo de error:

try
{
int bill = int.Parse(Console.ReadLine());
int people = int.Parse(Console.ReadLine());
Console.WriteLine("Por persona: " + bill / people + " €");
}
catch (FormatException)
{
Console.WriteLine("Escribe números enteros.");
}
catch (DivideByZeroException)
{
Console.WriteLine("Las personas no pueden ser 0.");
}
Entrada Salida
90 y 3 Por persona: 30 €
90 y 0 Las personas no pueden ser 0.
90 y tres Escribe números enteros.

Los bloques se comprueban de arriba abajo. Se ejecuta el primero cuyo tipo coincide, y solo ese. Los demás se saltan.

try
{
int bill = int.Parse(Console.ReadLine());
int people = int.Parse(Console.ReadLine());
Console.WriteLine("Por persona: " + bill / people + " €");
}
catch (FormatException)
{
Console.WriteLine("Escribe números enteros.");
}
catch (DivideByZeroException)
{
Console.WriteLine("Las personas no pueden ser 0.");
}
Paso a paso, en palabras
  1. Primera ejecución: cuenta de 90 €, las personas son 0.
  2. 90 / 0 es una división entre cero: se lanza DivideByZeroException.
  3. El primer catch es para FormatException. No coincide, se salta.
  4. El segundo coincide. Se ejecuta él y solo él.
  5. Segunda ejecución: en lugar del número de personas se ha escrito «tres».
  6. int.Parse no puede sacar un número: se lanza FormatException.
  7. Ya coincide el primer catch. Al segundo no se llega.
  8. Los bloques se comprueban de arriba abajo. Se ejecuta el primero que coincide.
Más precisamente, para curiosos
  • catch también puede tener una condición: catch (FormatException) when (text == ""). El bloque solo atrapa si la condición es verdadera.

La idea

La familia de las excepciones

Las excepciones son clases, relacionadas por herencia. Aquí están las más comunes:

La jerarquía
Exception
├── SystemException
│ ├── ArithmeticException
│ │ ├── DivideByZeroException
│ │ └── OverflowException
│ ├── FormatException
│ ├── IndexOutOfRangeException
│ ├── NullReferenceException
│ ├── ArgumentException
│ │ ├── ArgumentNullException
│ │ └── ArgumentOutOfRangeException
│ └── IOException archivos, red
└── ApplicationException

catch atrapa el tipo que nombras y todas sus clases hijas. Es el polimorfismo del módulo sobre la herencia: una variable de un tipo padre puede apuntar a un objeto de una clase hija. Por eso catch (Exception e) lo atrapa todo:

static void Split(string billText, string peopleText)
{
try
{
int bill = int.Parse(billText);
int people = int.Parse(peopleText);
Console.WriteLine("Por persona: " + bill / people + " €");
}
catch (Exception e)
{
Console.WriteLine(e.GetType().Name + ": " + e.Message);
}
}
Split("90", "3");
Split("90", "0");
Split("90", "tres");
Salida
Por persona: 30 €
DivideByZeroException: Attempted to divide by zero.
FormatException: The input string 'tres' was not in a correct format.

e.GetType().Name da el nombre de la clase real del objeto. La variable es de tipo Exception, pero el objeto es de una clase hija.

Más precisamente, para curiosos
  • StackOverflowException no se puede atrapar de ninguna manera: .NET detiene el programa enseguida. OutOfMemoryException sí, pero rara vez tiene sentido.
  • ApplicationException iba a ser la clase padre de las excepciones que definen los programas. En la práctica, tus propias excepciones las haces directamente a partir de Exception.
  • Atrapa de la forma más concreta que puedas. catch (Exception e) también esconde errores que no esperabas, como un fallo en tu propio código.

Pruébalo

¿Qué catch?

¿Qué imprimirá el programa?

int[] seats = { 4, 2, 6 };
try
{
Console.WriteLine("Mesa 4: " + seats[3] + " sitios");
}
catch (ArithmeticException)
{
Console.WriteLine("División entre cero");
}
catch (SystemException)
{
Console.WriteLine("Error de ejecución");
}
catch (Exception)
{
Console.WriteLine("Error general");
}
Respuesta

Pruébalo

Encuentra el error

Este programa no compila. ¿En qué línea está el error?

Elige la línea con el error.

La idea

El compilador no te obliga

Java tiene excepciones comprobadas: el compilador no acepta el programa hasta que las atrapas o las declaras. C# no tiene ninguna así. Ninguna excepción tiene que atraparse:

static void PrintReceipt()
{
Console.WriteLine("Imprimo el recibo...");
Thread.Sleep(300);
Console.WriteLine("Listo.");
}
PrintReceipt();
Salida
Imprimo el recibo...
Listo.

Thread.Sleep pausa el programa 300 milisegundos y puede lanzar una excepción, pero el compilador no pide nada. Lo mismo pasa con int.Parse.

Así que la responsabilidad es tuya. Qué excepciones lanza un método está escrito en su documentación, en la sección «Exceptions». Léela antes de decidir si atrapar.

La idea

finally: siempre

La impresora de la caja tiene que liberarse después de cada recibo, o el siguiente no se imprimirá. Pase lo que pase, salga bien o haya un error:

static void PrintReceipt(string qtyText)
{
Console.WriteLine("La impresora está ocupada.");
try
{
int qty = int.Parse(qtyText);
Console.WriteLine("Café: " + qty + " x 2 € = " + qty * 2 + " €");
}
catch (FormatException)
{
Console.WriteLine("Cantidad incorrecta: " + qtyText);
}
finally
{
Console.WriteLine("La impresora está libre.");
}
}
PrintReceipt("3");
PrintReceipt("tres");
Salida
La impresora está ocupada.
Café: 3 x 2 € = 6 €
La impresora está libre.
La impresora está ocupada.
Cantidad incorrecta: tres
La impresora está libre.

finally se ejecuta siempre: cuando todo sale bien, cuando la excepción se atrapa, e incluso cuando ninguno de estos bloques catch la atrapa. En este último caso finally se ejecuta, y la excepción sigue subiendo hasta el siguiente try que pueda atraparla.

¿Por qué no una simple línea después del try/catch? Porque esa línea se salta si la excepción es de un tipo que catch no atrapa, o si try tiene un return. finally no se salta.

Más precisamente, para curiosos
  • También puedes tener try con finally y sin catch: la excepción no se atrapa, pero la limpieza se ejecuta.
  • Para los recursos que hay que cerrar, como archivos y conexiones a bases de datos, hay una forma más corta: using. Te encontrarás con ella cuando trabajes con archivos y bases de datos. Cierra el recurso por sí sola, igual que lo haría finally.
  • finally no se ejecuta solo si se detiene todo el programa, por ejemplo con Environment.Exit(0).
  • Si la excepción no se atrapa en ningún sitio del programa, .NET puede detenerlo sin ejecutar los bloques finally. Así que atrapa las excepciones al menos en Main si hay algo importante en finally.

Pruébalo

finally y return

¿Qué imprimirá el programa?

static string Reserve(int guests)
{
try
{
if (guests > 6)
{
return "No hay mesa para " + guests;
}
return "Mesa para " + guests;
}
finally
{
Console.WriteLine("Registré el intento.");
}
}
Console.WriteLine(Reserve(8));
Respuesta

Ejercicio

Programas con varios bloques catch

Resuélvelas en tu editor. No imprimas mensajes para pedir datos.

1. Precio por persona. Los precios están en el array { 120, 75, 300 }. Lee un número de producto (desde 1) y el número de personas, e imprime el precio por persona con división entera. Atrapa tres cosas: un producto que no existe, cero personas, y todo lo demás (entonces imprime el mensaje de la excepción).

Entrada Salida
2 y 3 Por persona: 25 €
5 y 2 No existe ese producto.
1 y 0 El número de personas no puede ser 0.
1 y dos Error: The input string 'dos' was not in a correct format.

2. Calculadora. Lee un número entero, un operador (+, -, * o /) y un segundo número entero, cada uno en su propia línea. Imprime la cuenta. Con texto en lugar de un número di «Escribe números enteros.», con división entre cero «División entre cero.», y con cualquier otro operador «Operador desconocido: » seguido del operador.

Entrada
84
/
4
Salida
84 / 4 = 21
Entrada
84
/
0
Salida
División entre cero.

Más tareas

  • El datáfono. El método Pay(string amountText) imprime «El datáfono está conectado.», después «Pagado: X €» o un mensaje sobre la entrada incorrecta, y en finally «El datáfono está apagado.». Llámalo con «25» y con «veinticinco».
  • Nota media sin caídas. Lee 5 líneas con notas. Sáltate las que no son números enteros (una excepción) o no están entre 2 y 6 (un if). Imprime la media con división entera. Si ninguna nota es válida, la división entre cero lanza: atrápala e imprime «No hay notas válidas.»
  • Predice y comprueba. Escribe un try con catch y finally, donde cada bloque imprime una línea. Predice la salida cuando sale bien, con una excepción atrapada y con una no atrapada, y después ejecútalo.

Test

Ponte a prueba

1. La excepción coincide con dos de los tres bloques catch. ¿Cuál se ejecuta?
2. ¿Atrapa catch (SystemException e) una excepción de tipo FormatException?
3. ¿Por qué catch (Exception e) tiene que ir el último?
4. ¿Cuándo se ejecuta finally?
5. ¿Te obliga el compilador de C# a atrapar FormatException alrededor de int.Parse?

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