Przechwytywanie wyjątków pozwala programistom rozwiązać problem związany z tym, że w każdej aplikacji może wystąpić nieprzywidziana sytuacja.
Przykładowo jeśli chcesz otworzyć plik używając FileReader z Javy, albo StreamReader z C#, to istnieje kilka sytuacji, które mogą wywołać błąd działania, czyli wyjątek. Przykładowo plik może nie istnieć.
package cez;
import java.io.FileReader;
public class MyApp
{
public static void main(String[] args)
{
FileReader in = new FileReader("niema.file"); // error
}
}
using System.IO;
namespace ConsoleApplication_CSharpFun
{
class Program
{
static void Main(string[] args)
{
StreamReader sr = new StreamReader("niema.txt");
}
}
}
Od razu widać pierwszą różnicę pomiędzy Javą, a C#, a raczej filozofią dotyczącą wyjątków.
Eclipse lub inny Intellisense przypomni nam, że podane polecenie może wrócić wyjątek “FileNotFoundException”. Kompilatory Javy nie pozwolą nam skompilować takiego kodu dopóki nie umieścimy bloku Try/Catch
Java nie wymusza obsługi wszystkich wyjątków na programiście. Głównie chodzi o wyjątki, które dziedziczą po RunTimeException i są bardziej spowodowane błędnym kodem niż nieprzywidzianym działaniem użytkownika aplikacji. Przykładami takich wyjątków są NullPointerException, ArrayIndexOutOfBoundException.
Są to wyjątki z grupy unchecked exceptions, które nie wymagają obsługi.
Wyjątek NullPointerException nigdy by nie nastąpił, gdybyś sprawdzał istnienie referencji obiektu. Wyjątek ArrayIndexOutOfBoundException nigdy nie zostanie rzucony jeśli będziesz sprawdzał indeks w tablicy. Są to wyjątki checked.
W Visual Studio nie ma takiej informacji. Dotyczy to wszystkich wyjątków. Kompilator C# nie wymusza na programiście obsługi wyjątków.
Tobie czytelniku zostawiam do oceny, które zachowanie jest bardziej przyjazne i pomocne w tworzeniu w aplikacji.
Try-catch
Aby więc zabezpieczyć program przed wyjątkami musimy je otoczyć blokami try i catch.
Wewnątrz bloku try umieszczamy kod, który potencjalnie wyrzuci wyjątek. Jeśli kod wewnątrz bloku try rzeczywiście nie zadziała poprawnie, wtedy obsługa wyjątku leży w bloku Catch.
Dzięki jednak blokowi Try-Catch program będzie mógł działać dalej i wykonywać dalsze polecenia.
package cez;
import java.io.FileNotFoundException;
import java.io.FileReader;
public class MyApp
{
public static void main(String[] args)
{
try {
FileReader in = new FileReader("brak.file");
}
catch(FileNotFoundException e) {}
}
}
Wewnątrz bloku Catch możemy np. wyświetlić informację dla użytkownika, że błąd wystąpił.
using System;
using System.IO;
namespace ConsoleApplication_CSharpFun
{
class Program
{
static void Main(string[] args)
{
try
{
StreamReader sr = new StreamReader("niema.txt");
}
catch
{
Console.Write("Nie ma pliku");
}
}
}
Catch
Blok Catch łapie wyjątek i jeśli wyjątek nie został określony domyślnie, on złapie wszystkie wyjątki. W C# i w Javie wszystkie wyjątki dziedziczą po Exception.
Oznacza to, że poniższe wyrażenie złapie wszystkie wyjątki.
catch(Exception){};
Aby złapać konkretne wyjątki musisz określić się bardziej specyficznie. Wyjątki są ustawiane od najbardziej specyficznych do tych najmniej. Wyrażenia od góry są sprawdzane najpierw.
catch (FileNotFoundException) {}
catch (Exception) {}
Opcjonalnie blok Catch może definiować obiekt, który będzie przekazywał informację na temat wyjątku. W ten sposób uzyskujemy informację na temat komunikatu błędu danego wyjątku.
catch (Exception e)
{
Console.Write("Error: " + e.Message);
}
Te same zasady tyczą także Javy.
catch(FileNotFoundException e) {
System.out.print(e.getMessage());
}
catch(Exception e) {
System.out.print(e.getMessage());
}
Blok finally
Istnieje jeszcze blok finally, który jest blokiem opcjonalnym i może zostać umieszczony zaraz po bloku Try, jeśli blok catch nie istnieje albo po bloku Catach, jeśli on występuje.
Ten blok jest używany do oczyszczania zasobów stworzonych w bloku try. Blok finally zawsze się wykona nawet jeśli wcześniej w metodzie było polecenie return.
Po co jest ten blok? Otóż przykładowo jeśli otworzyliśmy plik to wydaje się naturalne, że powinniśmy go zamknąć. Problem polega na tym, że po procesie otwarcia wiele rzeczy może się wydarzyć, w tym te nie przewidziane.
Musimy mieć jakąś gwarancję, że cokolwiek by się nie stało po otwarciu pliku, plik zostanie zamknięty i blok finally daje taką opcję.
Zmienna referująca się do pliku musi być jednak przed blokiem TRY.
static void Main(string[] args)
{
StreamReader sr = null;
try
{
sr = new StreamReader("niema.txt");
}
catch (FileNotFoundException) { }
finally
{
if (sr != null) sr.Close();
}
}
Kod Javy wymusza jeszcze na programiście postawienie bloku TRY/Catch wewnątrz bloku finally ponieważ samo zamkniecie pliku może wywołać także wyjątek.
package cez;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
public class MyApp
{
public static void main(String[] args)
{
FileReader in = null;
try {
in = new FileReader("niema.file");
}
catch(FileNotFoundException e) {
System.out.print(e.getMessage());
}
finally {
if (in != null) {
try { in.close(); }
catch(IOException e) {}
}
}
}
}
Oto kolejny przykład użycia bloku finally w C#. Samo utworzenie Bitmapy nigdy nie powinno wywołać wyjątku jednakże ten zasób trzeba zwolnić z pamięci ręcznie za pomocą metody Dispose().
Dlatego tym razem skorzystałem z wyrażenia Try/Finally
Aby utworzyć obiekt klasy Bitmap muszę do projektu dodać referencję System.Drawing.
using System;
using System.Drawing;
using System.IO;
namespace ConsoleApplication_CSharpFun
{
class Program
{
static void Main(string[] args)
{
Bitmap bit = null;
try
{
bit = new Bitmap(300, 300);
System.Console.WriteLine("Width: " + bit.Width +
", Height: " + bit.Height);
}
finally
{
if (bit != null) bit.Dispose();
}
}
}
}
Łapianie wiele wyjątków nie dziedzicząych po sobie na raz
Java może przechwytywać różne typy wyjątków niedziedziczących po sobie w jednym bloku catch
try { MakeException() ; } catch (ArithmeticException | IOException e) { // TODO Auto-generated catch block e.printStackTrace(); }
W C# do wersji 6.0 nie było to możliwe i trzeba było pisać taki kod:
catch (Exception ex)
{
if (ex is FormatException || ex is OverflowException)
{
WebId = Guid.Empty;
return;
}
throw;
}
W C# 6.0 jednak jest to możliwe:
catch (Exception ex) when (ex is FormatException || ex is OverflowException)
{
}
Wyrażenie Using : tylko C#
C# oferuje wyrażenie Using. Jest to uproszczona składnia polecenia Try/finally.
Zaczynamy od słowa using, a wewnątrz w nawiasach deklarujemy utworzenie danego zasobu. Ciało kodu, który korzysta z danego zasobu jest deklarowane wewnątrz nawiasów klamrowych.
using (Bitmap b = new Bitmap(300, 300))
{
System.Console.WriteLine("Width: " + b.Width +
", Height: " + b.Height);
}
Kiedy wykonywanie kodu bloku using się skończy C# automatycznie wykona metodę Dispose() . Dla pliku metoda Dispose zawiera operację zamknięcia pliku więc wewnątrz bloku using nie zamykamy pliku.
Wyrzucanie wyjątków
Czasem w naszym kodzie, gdy coś nie będzie się działo po naszej myśli sami chcemy wrzucać wyjątki. Wyjątki to tak naprawdę obiekty dlatego do ich utworzenia korzystamy ze słowa kluczowego “new”.
Aby wyrzucić wyjątek korzystamy ze słowa kluczowego throw.
static void GiveError()
{
throw new System.DivideByZeroException("Universe Explodes");
}
Zachowanie to jest takie samo w Javie i w C#.
Wyjątek wtedy rozprzestrzeni się w górę w stosie wywołań, aż zostanie złapany. Jeśli nie zostanie on złapany wtedy cała aplikacja zostanie zamknięta.
Wewnątrz bloku catch można ponownie wyrzucić przechwycony wyjątek bez utraty stosu wywołań (CallStack). Robimy to poprzez użycie słowa kluczowego “throw”
static void Main() { try { GiveError(); } catch { throw; } }
Pamiętaj, że jeśli w Javie wyrzucasz wyjątek typu checked, wtedy jeszcze zmuszony jesteś go obsłużyć. Chociaż jeśli już świadomie wyrzucasz wyjątek, nieważne czy typu Checked, czy unchecked, to wypadało go obsłużyć wyżej w przepływach wywołań metod.
Możesz też w sygnaturze metody określić, że dana metoda może wyrzucić dane wyjątki. Jeśli tak zrobisz to odpowiedzilaność obsługi przechodzi dalej do następnej metody, która będzie korzystała z tej metody.
package cez;
import java.io.IOException;
public class MyApp
{
public static void main(String[] args)
{
try {
MakeException() ;
} catch (ArithmeticException | IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
static void MakeException() throws IOException, ArithmeticException
{
int a = 0;
if (a == 0 ) {
throw new IOException("My IO exception");
} else {
throw new ArithmeticException("Division by zero");
}
}
}
Wyjątki typu : Checked i unchecked
Wyjątki w Javie można podzielić na Checked i unchecked. Tak jak wyjaśniliśmy wcześniej wyjątki checked wymuszają na programiście ich obsługę. Przykładowo takim wyjątkiem jest IOException, który określa wszystkie wyjątki związane z operacjami na plikach.
Wyjątki Unchecked jak np. wyjątek ArithmeticException - nie wymuszają na programiście ich obsługi. Ma to sens bo, gdyby taka było każda operacja arytmetyczna, w teorii musiałaby być wykonywana wewnątrz bloku try/catch.
Dziedziczenie wyjątków
W C# wszystkie wyjątki dziedziczą po Exception. Utworzenie więc swojego wyjątku nie jest takie skomplikowane.
[Serializable]
public class MyException : Exception
{
public MyException ()
{}
public MyException (string message)
: base(message)
{}
public MyException (string message, Exception innerException)
: base (message, innerException)
{}
protected MyException (SerializationInfo info, StreamingContext context)
: base (info, context)
{}
}
W Javie sprawa jest trochę bardziej skomplikowana.
Wyjątki dziedziczące po klasie Error określają błędy, po których aplikacja nie będzie mogła działać dalej. Przykładem takiego wyjątku jest “OutOfMemeoryError” wystąpi on, gdy aplikacja nie dostanie wystarczającej pamięci do operacji X.
Wyjątki z klasy Error są wyjątkami unchecked, gdyż programista i tak nie może nic z nimi zrobić.
Wyjątki dziedziczące po Exception i po RuntimeExcpetion są również wyjątkami unchecked. Określają one błędy wynikające głównie ze złego kodu w aplikacji.
Wszystkie inne wyjątki dziedziczące tylko po Exception są wyjątkami checked i trzeba je obsłużyć. Aplikacja powinna po nich działać dalej.
Oto moje wyjątki napisane w Javie. Zauważ, że mój wyjątek KateException nie będzie wymagał obsługi.
public class CezException extends Exception {
private static final long serialVersionUID = 3L;
public CezException() { super(); }
public CezException(String message) { super(message); }
public CezException(String message, Throwable cause) { super(message, cause); }
public CezException(Throwable cause) { super(cause); }
}
public class KateException extends RuntimeException {
private static final long serialVersionUID = 2L;
public KateException() { super(); }
public KateException(String message) { super(message); }
public KateException(String message, Throwable cause) { super(message, cause); }
public KateException(Throwable cause) { super(cause); }
}
Użycie.
package cez;
import java.io.IOException;
public class MyApp
{
public static void main(String[] args)
{
try {
MakeException() ;
} catch (CezException e ) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
static void MakeException() throws CezException
{
int a = 0;
if (a == 0 ) {
throw new CezException();
} else {
throw new KateException();
}
}
}