wtorek, 29 listopada 2016

Typy wyjątków

Poprzednia notatka zawierała informacje związane z obsługą wyjątków oraz ich zgłaszaniem. Cofnijmy się o krok, aby zadać sobie pytanie: W jakich sytuacjach dochodzi do zgłaszania wyjątków?

  • Identyfikacja problemu przez nasz kod. Bardzo często sami będziemy chcieli reagować na nieprawidłowe sytuacje, tak aby przerwać działanie i przekazać informację.
  • Użycie bibliotek API. Przykładem może być sytuacja, w której próbujemy otworzyć nieistniejący plik. Pomimo tego, że kod wygląda poprawnie, następuje zgłoszenie wyjątku.
  • Środowisko wykonawcze wykrywa niepowodzenie, np. próbę użycia pustej referencji lub próbę podzielenia przez zero.
  • Środowisko wykonawcze wykrywa sytuację, nad którą nie mamy kontroli (związaną z wyjątkami asynchronicznymi).

Zajmijmy się pierwszym punktem, czyli sytuacją, gdy nasz kod zidentyfikuje problem. W celu zgłoszenia wyjątku użyjemy słowa kluczowego throw, po którym utworzymy instancję obiektu dziedziczącego z klasy Exception. Pytanie, którego typu użyć? Udostępnione są setki wyjątków, i nie mamy konieczności poznania ich wszystkich, ale powinniśmy posiadać wiedzę odnośnie podstawowych typów wyjątków. Zacznijmy od ArgumentException, który stanowi klasę bazową dla wyjątków związanych z niepoprawnie przekazanym argumentem. Mieliśmy również okazję poznać ArgumentNullException, który pojawia się w sytuacji, gdy przekazana wartość jest null'owa. Kolejnym wyjątkiem z tej grupy jest ArgumentOutOfRangeException. Wymienione wyjątki dziedziczą z klasy ArgumentException, która udostępnia właściwość ParamName, identyfikującą parametr związany ze zgłoszonym wyjątkiem.

Kolejnym często stosowanym wyjątkiem jest InvalidOperationException, informujący o próbie wykonania operacji niemożliwej przez aktualny stan obiektu. Wyobraźmy sobie, że implementujemy klasę reprezentującą urządzenie elektroniczne. Jeżeli urządzenie jest już włączone, nie możemy ponownie go włączyć. Warto znać różnicę pomiędzy InvalidOperationException a NotImplementedException. Ten drugi wskazuje na fakt, że dana składowa nie została jeszcze zaimplementowana. Visual Studio używa wyjątku NotImplementedException podczas automatycznego generowania kodu, tak aby zbudować szkielet, który może zostać zaimplementowany w późniejszym czasie. Przed udostępnieniem aplikacji należy pozbyć się wszystkich wyjątków NotImplementedException.

Mogą pojawić się sytuacje, w których świadomie będziemy chcieli skorzystać z typu NotImplementedException. Przykładem może być sytuacja, w której odpowiadamy za implementację abstrakcyjnej klasy bazowej wykorzystywanej przez innych programistów. Z czasem prawie na pewno pojawi się konieczność dodania nowej funkcjonalności. Dodanie nowej metody do klasy bazowej wiązałoby się z koniecznością modyfikacji kodu korzystającego z naszej klasy. Problemu można uniknąć, korzystając z metody wirtualnej, gdzie w klasie bazowej ograniczamy się do zgłoszenia wyjątku typu NotImplementedException. Implementacja przeniesiona zostanie na klasy dziedziczące. Mając na uwadze dobre praktyki programistyczne, implementujmy kod zgodnie z zasadami SOLID, korzystając ze wzorców projektowych.

A co jeżeli potrzebujemy czegoś więcej niżeli udostępnione typy wyjątków? Możemy utworzyć własny typ, pamiętając, że musi on dziedziczyć z Exception. Jeżeli sygnalizowany problem dotyczy już istniejącego wyjątku, ale brakuje nam dodatkowej informacji, możemy dziedziczyć z wyspecjalizowanego wyjątku. Pomimo udostępnienia konstruktora bezargumentowego raczej się z niego nie korzysta. Wyjątek powinien posiadać czytelny opis problemu. Dodatkowo powinniśmy zapewnić możliwość przekazania pierwotnego wyjątku. Przytoczony został przykład zgłoszenia wyjątku podczas kolejnej próby włączenia urządzenia elektrycznego, poniżej przykład implementacji typu wyjątku umożliwiającego obsługę takiej sytuacji.

public class DeviceInvalidOperationException : InvalidOperationException
{
    public DeviceInvalidOperationException(DeviceState state) 
        : this("Improper condition of the device", state)
    {

    }

    public DeviceInvalidOperationException(string message, DeviceState state)
        : base (message)
    {
        State = state;
    }

    public DeviceInvalidOperationException(string message, DeviceState state, Exception innerEx)
        : base(message, innerEx)
    {
        State = state;
    }

    public DeviceState State { get; private set; }
}

public enum DeviceState
{
    None,
    Failed,
    TurnedOn,
    TurnedOff
}

Utworzony typ wyjątku posiada właściwość State, która rozszerza bazowy typ, dzięki czemu mamy szerszy zestaw informacji. Z technicznego punktu, brakuje serializacji, która umożliwia przeniesienie kompletu informacji pomiędzy procesami czy nawet urządzeniami. W celu zapewnienia wspomnianej serializacji musimy skorzystać z atrybutu [Serializable]. Poniżej przykład zastosowania atrybutu umożliwiającego skonwertowanie wyjątku do strumienia danych.

[Serializable]
public class DeviceInvalidOperationException : InvalidOperationException
{
...
}

Na początku notatki wymienione zostały sytuacje, podczas których dochodzi do zgłaszania wyjątków. Jedna z nich dotyczy sytuacji, w których środowisko wykonawcze wykrywa zdarzenie, nad którym nie mamy kontroli. Dotyczy to wyjątków asynchronicznych i nie ma to nic wspólnego ze słowem kluczowym async. Asynchroniczność tyczy się sytuacji, które mogą doprowadzić do zgłoszenia wyjątku niezależnie od tego, czym zajmuje się nasz kod. Wyjątki, które mogą zostać zgłoszone w ten sposób, to: ThreadAbortException, OutOfMemoryException oraz StackOverflowException.

Wyjątek ThreadAbortException występuje wraz z zakończeniem wątku przez inny wątek. Kod poniżej uruchamia wątek, którego jedynym zadaniem jest cykliczne wyświetlenie napisu "Sleep...". Po uruchomieniu wątku czekamy 5 sekund, a następnie wywołujemy metodę Abort. Przed uruchomieniem przykładu należy "poprosić" Visual Studio o zatrzymanie debugowania wraz ze zgłoszeniem wyjątku. W tym celu z górnego menu wybieramy Debug / Windows / Exception Settings. W wyświetlonym oknie zaznaczamy opcję Common Language Runtime Exceptions. Po zaznaczeniu za każdym razem, gdy zgłoszony zostanie wyjątek, debugger zatrzyma kod. W oknie Exception Settings możemy określić, dla jakich typów wyjątków chcemy, aby środowisko informowało o zgłoszeniu wyjątku.

static void Main(string[] args)
{
    var thread = new Thread(() =>
    {
        while (true)
        {
            Thread.Sleep(1000);
            Console.WriteLine("Sleep...");
        }
    });
    thread.Start();

    Console.WriteLine("Wait 5 seconds and abort thread");
    Thread.Sleep(5000);
    thread.Abort();
}

Pozostałe dwa wyjątki pojawiają się wraz z przekroczeniem dostępnej pamięci lub w przypadku przepełnienia stosu. Sytuacja jest o tyle ciekawa, że oba wyjątki mogą pojawić się niezależnie od wykonywanych operacji. CLR rezerwuje sobie prawo do dynamicznego powiększenia stosu oraz rezerwacji dodatkowej pamięci. Z tego powodu wyjątki określane są jako asynchroniczne. Na domiar złego, zgłoszenie wyjątku może pojawić się w sekcji finally — tej utworzonej ręcznie, jak i tej wygenerowanej automatycznie na potrzeby using. Jak poradzić sobie z taką sytuacją? Najpopularniejszym rozwiązaniem jest akceptacja porażki. W sytuacji, gdy wyjątek OutOfMemoryException pojawia się wraz z próbą zarezerwowania dużego obszaru pamięci, prawdopodobne jest, że pomimo jego wystąpienia, aplikacja będzie działać dalej. Jeżeli jednak błąd występuje przy próbie alokowania pamięci dla małych obiektów, najbardziej sensownym rozwiązaniem może okazać się zakończenie aplikacji.

Na koniec jeszcze jeden aspekt związany z wyjątkami. CLR pozwala na rejestrację wyjątków, które nie zostały obsłużone i dotarły do początku stosu wywołań. W takiej sytuacji, przed samym zatrzymaniem aplikacji, zgłaszane jest zdarzenie UnhandledException udostępnione przez klasę AppDomain.CurrentDomain. Jej zadaniem jest umożliwienie zapisania informacji dotyczących awarii.

Informacje zaprezentowane w tej notatce uzupełniają pierwszy wpis dotyczący obsługi wyjątków. Materiał można potraktować jako wprowadzenie do zagadnienia zarządzania wyjątkami.

Troska Robert