Blog Dla Młodszych Programistów C#/.NET

1 września 2020
Nieodłącznym elementem pracy programistów jest naprawianie bugów w aplikacji. Nie jest tak łatwo przewidzieć wszystkich scenariuszy, w jaki sposób użytkownicy będą pracować na Twojej aplikacji. Dlatego czasem (a może nawet często) zdarza się, że zgłaszają oni błędy (które niekoniecznie muszą występować z winy programisty). Jeżeli użytkownik poinformuje Cię o tym, że w aplikacji wystąpił jakiś błąd, to bez szczegółowych informacji, może Ci być ciężko ten błąd naprawić. Czasem jest nawet tak, że ciężko wywołać ten błąd ponownie. Oczywiście w niektórych sytuacjach, jeżeli nie jest to błąd krytyczny, to nawet użytkownik może Cię o tym błędzie nie poinformować. Jednak zawsze wtedy tracimy trochę w oczach naszych klientów. Dlatego niezwykle ważnym elementem, jest odpowiednie przechwytywanie i obsługa wyjątków. O tym, jak robić to poprawnie, dowiesz się właśnie z tego artykułu.

Proste zasady, o których musisz pamiętać, podczas obsługi wyjątków w C#


Przykład


Przygotowałem krótki kod w C#. Jest to aplikacja konsolowa, która w statycznej metodzie Main wywołuje metodę odpowiedzialną za wysyłanie maili. Metoda Send powinna wykonywać logikę odpowiedzialną za wysłanie maila. W ciele metody Send jest wywoływana jeszcze metoda Connect na obiekcie klasy HostSmtp, która w naszym przypadku symuluje tylko wystąpienie błędu z wiadomością Cannot connect.
using System;

namespace App
{
    public class HostSmtp
    {
        public void Connect()
        {
            throw new Exception("Cannot connect.");
        }
    }

    public class EmailSender
    {
        public void Send()
        {
            new HostSmtp().Connect();
        }
    }

    public class Program
    {
        static void Main(string[] args)
        {
            new EmailSender().Send();
        }
    }
}

Po uruchomieniu tego programu otrzymujemy następujący wynik w konsoli:
Unhandled Exception: System.Exception: Cannot connect.
   at App.HostSmtp.Connect() in C:\ConsoleApp\Program.cs:line 9
   at App.EmailSender.Send() in C:\ConsoleApp\Program.cs:line 17
   at App.Program.Main(String[] args) in C:\ConsoleApp\Program.cs:line 25

To akurat nie było zaskoczeniem, można było taką sytuację przewidzieć. Przyznasz jednak, że takie zachowanie aplikacji po pierwsze nie jest zbyt przyjazne dla użytkownika, a po drugie nie powiadamia administratora o jakimś nieprzewidzianym zachowaniu aplikacji. Dlatego, jeżeli jest jakiś kod, gdzie spodziewamy się otrzymać błąd, to możemy wywołać ten kod w bloku try catch.


Czy użycie bloku try catch wystarczy?


Dzięki zastosowaniu bloku try catch nasze wywołanie w metodzie Send może wyglądać w ten sposób:
public class EmailSender
{
    public void Send()
    {
        try
        {
            new HostSmtp().Connect();
        }
        catch (Exception)
        {
        }
    }
}

Ok, teraz uruchamiamy ponownie naszą aplikację. Nie ma żadnych błędów, więc chyba wszystko dobrze? No, nie do końca :) Właściwie to takie, rozwiązanie jest jeszcze gorsze niż poprzednie, ponieważ co prawda przechwyciliśmy wyjątek, ale nie został on przez nas odpowiednio obsłużony. Mail, który miał być wysłany - nie został wysłany oraz nie zostaliśmy o tym fakcie poinformowani. Także, to rozwiązanie jest fatalne. Staraj się unikać takiej sytuacji, nigdy nie ma dobrego powodu, aby użyć takiego właśnie zapisu. W takim razie jak obsłużyć ten wyjątek? Mamy tutaj 2 problemy, które musimy rozwiązać. Chcemy najpierw, aby administrator systemu mógł przejrzeć szczegółowe informacje o błędzie oraz, żeby użytkownikowi wyświetlił się bardziej przyjazny komunikat, tutaj w szczególności bez informacji o całym stosie błędu.


Zapisywanie błędów


Pierwszy problem można łatwo rozwiązać, wystarczy użyć jednego z wielu frameworków do logowania danych i zapisać wszystkie szczegółowe informacje o błędzie, tak żeby łatwo dało się zdiagnozować przyczynę błędu. Ten kod może wyglądać tak:
public class EmailSender
{
    public void Send()
    {
        try
        {
            new HostSmtp().Connect();
        }
        catch (Exception ex)
        {
            //Zapisanie wszystkich szczegółowych informacji o błędzie
            Logger.Error("dodatkowe-informacje-o-błędzie", ex);
        }
    }
}

Udało się zapisać do pliku wszystkie szczegółowe informacje o błędzie, jest już lepiej, ale dalej mamy problem z tym, że błąd został przechwycony, ale nie został prawidłowo obsłużony. Dalej użytkownik nie wie, że operacja się nie powiodła.


Popularne sposoby obsługi wyjątków


Jest dużo sposobów, jak można obsłużyć jeszcze ten błąd. Przyjrzyjmy się 4 najczęściej używanym rozwiązaniom, a następnie wybierzemy najlepsze.

1 sposób "throw ex":
public class EmailSender
{
    public void Send()
    {
        try
        {
            new HostSmtp().Connect();
        }
        catch (Exception ex)
        {
            //Zapisanie wszystkich szczegółowych informacji o błędzie
            Logger.Error("dodatkowe-informacje-o-błędzie", ex);
            throw ex;
        }
    }
}

Wynik:
Unhandled Exception: System.Exception: Cannot connect.
   at App.EmailSender.Send() in C:\ConsoleApp\Program.cs:line 25
   at App.Program.Main(String[] args) in C:\ConsoleApp\Program.cs:line 34

Uwagi:
Niestety używając throw ex; nie mamy pełnej informacji o błędzie, który wystąpił. Nie ma informacji o tym, że wyjątek został rzucony w metodzie Connect w linii 9, przez to zdiagnozowanie błędu może być trudniejsze. Ten sposób jest zły.

2 sposób "throw new Exception("Some exception.")":
public class EmailSender
{
    public void Send()
    {
        try
        {
            new HostSmtp().Connect();
        }
        catch (Exception ex)
        {
            //Zapisanie wszystkich szczegółowych informacji o błędzie
            Logger.Error("dodatkowe-informacje-o-błędzie", ex);
            throw new Exception("Some exception.");
        }
    }
}

Wynik:
Unhandled Exception: System.Exception: Some exception.
   at App.EmailSender.Send() in C:\ConsoleApp\Program.cs:line 25
   at App.Program.Main(String[] args) in C:\ConsoleApp\Program.cs:line 34

Uwagi:
Podobnie jak w poprzednim przykładzie, gdy używamy throw new Exception, tracimy informację o błędzie, który wystąpił wcześniej. Także, ten sposób również jest zły.

3 sposób "throw":
public class EmailSender
{
    public void Send()
    {
        try
        {
            new HostSmtp().Connect();
        }
        catch (Exception ex)
        {
            //Zapisanie wszystkich szczegółowych informacji o błędzie
            Logger.Error("dodatkowe-informacje-o-błędzie", ex);
            throw;
        }
    }
}

Wynik:
Unhandled Exception: System.Exception: Cannot connect.
   at App.HostSmtp.Connect() in C:\ConsoleApp\Program.cs:line 9
   at App.EmailSender.Send() in C:\ConsoleApp\Program.cs:line 25
   at App.Program.Main(String[] args) in C:\ConsoleApp\Program.cs:line 34

Uwagi:
I o to nam chodziło. W przypadku użycia samego throw mamy cały stack trace, wszystkie informacje o wcześniejszych błędach. Jak widzisz tylko w tym przypadku, nie przepadła informacja o pierwszym błędzie, który wystąpił w metodzie Connect w lini 9. Także, z tych 3 sposobów, często spotykanych w różnych aplikacjach, tylko ten sposób jest prawidłowy.

4 sposób "throw new Exception("Some exception.", ex)":
public class EmailSender
{
    public void Send()
    {
        try
        {
            new HostSmtp().Connect();
        }
        catch (Exception ex)
        {
            //Zapisanie wszystkich szczegółowych informacji o błędzie
            Logger.Error("dodatkowe-informacje-o-błędzie", ex);
            throw new Exception("Some exception.", ex);
        }
    }
}

Wynik:
Unhandled Exception: System.Exception: Some exception. ---> System.Exception: Cannot connect.
   at App.HostSmtp.Connect() in C:\ConsoleApp\Program.cs:line 9
   at App.EmailSender.Send() in C:\ConsoleApp\Program.cs:line 19
   --- End of inner exception stack trace ---
   at App.EmailSender.Send() in C:\ConsoleApp\Program.cs:line 25
   at App.Program.Main(String[] args) in C:\ConsoleApp\Program.cs:line 34

Uwagi:
Jak widzisz, jest jeszcze 4 sposób obsługi wyjątków. Ten sposób jest również dobry, ale raczej używa się go tylko wtedy gdy chcemy rzucić wyjątek innego typu.


Wyświetlenie komunikatu użytkownikowi


Oczywiście użytkownikowi nie możemy wyświetlić takiego błędu, powinniśmy wyświetlić odpowiedni komunikat, co najczęściej robi się w metodzie globalnej, która w zależności od rodzaju aplikacji wygląda inaczej. W naszym przypadku, w aplikacji konsolowej może wyglądać w ten sposób:
public class Program
{
    static void Main(string[] args)
    {
        AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;

        new EmailSender().Send();
    }

    private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
    {
        Logger.Error(ex. ExceptionObject);
        Console.WriteLine("Wystąpił nieobsłużony błąd.");
        Environment.Exit(1);
    }
}

Lub możesz obsłużyć bezpośrednie wywołanie:
public class Program
{
    static void Main(string[] args)
    {
        try
        {
            new EmailSender().Send();
        }
        catch (Exception ex)
        {
            Console.WriteLine("Nie udało się wysłać maila.");
        }
    }
}

Dzięki takiemu rozwiązaniu aplikacja działa dalej poprawnie, a użytkownik zostaje poinformowany o błędzie. Wyświetliłem tutaj ogólny błąd, ale dzięki właściwości Message każdego wyjątku możesz też wyświetlić bardziej szczegółowe informacje.

Także, cały kod może wyglądać w ten sposób:
using System;

namespace App
{
    public class HostSmtp
    {
        public void Connect()
        {
            throw new Exception("Cannot connect.");
        }
    }

    public class EmailSender
    {
        public void Send()
        {
            try
            {
                new HostSmtp().Connect();
            }
            catch (Exception ex)
            {
                //Zapisanie wszystkich szczegółowych informacji o błędzie
                Logger.Error("dodatkowe-informacje-o-błędzie", ex);
                throw;
            }
        }
    }

    public class Program
    {
        static void Main(string[] args)
        {
            try
            {
                new EmailSender().Send();
            }
            catch (Exception ex)
            {
                Console.WriteLine("Nie udało się wysłać maila.");
            }
        }
    }
}



PODSUMOWANIE:


Mam nadzieję, że w tym artykule udało mi się pokazać Ci jak prawidłowo obsługiwać wyjątki w C#. Pamiętaj, żeby nigdy nie pozostawiać pustej klauzuli catch, ponieważ wtedy błąd zostanie stłumiony, a nie zostanie w żaden sposób obsłużony. Zawsze powinno się zapisywać jak najwięcej informacje o błędach, dzięki czemu ich wykrycie oraz poprawienie będzie dużo łatwiejsze. Zaoszczędzi Ci to w przyszłości sporo czasu. Pokazałem Ci kilka sposobów jak takie wyjątki w odpowiedni sposób obsłużyć. Pamiętaj, że zazwyczaj najlepszym sposobem jest użycie po prostu throw, dzięki czemu nie zostaną utracone żadne informacje o wcześniejszym błędzie.

Poprzedni artykuł - Jak Tworzyć Nowe Klasy w Visual Studio Domyślnie z Modyfikatorem Public?.
Następny artykuł - Proste Logowanie Danych Do Pliku w C# Za Pomocą Biblioteki NLog.
Autor artykułu:
Kazimierz Szpin
Kazimierz Szpin
Programista C#/.NET. Głównie pisze aplikacje w ASP.NET MVC, WPF oraz Windows Forms. Specjalizuje się w testach jednostkowych.
Autor bloga ModestProgrammer.pl
Komentarze (4)
Cepewka
CEPEWKA, 2 września 2020 17:18
throw; też jest słabe, bo gubi szczegóły przy wielokrotnych wywołaniach. W którymś nowym .NET Core to zmienili, ale ciągle jest Framework, Mono i inne implementacje, które mają to skopane. Lepiej użyć ExceptionDispatchInfo.Capture(e).Throw(); Co do używania Environment.Exit, to jest to raczej słaby pomysł - w aplikacji może być wiele appdomen, może być wiele handlerów dla OnUnhandledException, zabijanie programu z powodu wyjątku jest co najmniej dyskusyjne.
Empek
EMPEK, 5 września 2020 17:21
Dobry art, patrzac z poziomu uczacego sie programowania nie mysli sie duzo o urzytkowniku (bo jestes jedynym :D ) a to przypomina o dobrych praktykach
Kazimierz Szpin
KAZIMIERZ SZPIN, 5 września 2020 19:32
Cześć @CEPEWKA, prawdę mówiąc nie miałem nigdy problemów z samym throw. Nie używałem wcześniej ExceptionDispatchInfo.Capture(e).Throw(); :) Jeżeli chodzi o Environment.Exit to po prostu chciałem pokazać przykładową globalną implementację nieobsłużonych wyjątków, oczywiście nie trzeba zamykać aplikacji gdy wystąpi nieobsłużony wyjątek :) Dzięki!
Kazimierz Szpin
KAZIMIERZ SZPIN, 5 września 2020 19:33
Cześć @EMPEK, i dzięki! Cieszę się, że artykuł Ci się spodobał :)
Dodaj komentarz

Wyszukiwarka

© Copyright 2020 modestprogrammer.pl. Wszelkie prawa zastrzeżone. Polityka prywatności. Design by Kazimierz Szpin