Witaj na Zine.net online Zaloguj się | Rejestracja | Pomoc

C#: throw; vs throw e;

Jakoś ostatnio często wpadają mi do rąk różne teksty o wyjątkach w .Net. Z tego co da się zauważyć, to wyjątki, mimo swej prostej koncepcji, nie są do końca rozumiane i dobrze używane. Na szczęście są tacy ludzie jak Krzysztof Cwalina, którzy pomagają biednym programistom. Bardzo przydatne informacje na temat wyjątków można znaleźć na jego blogu. Ale nie o tym chciałem pisać...

Bardziej zaawansowanych programistów C# może nie zaskoczę, ale na pewno znajdzie się ktoś, kto nie do końca czuje różnicę (a może jej nie ma?).

Jeżeli złapiemy wyjątek w blok try-catch i mimo wszystko nie jesteśmy w stanie go do końca obsłużyć to powinniśmy go puścić dalej. Początkujący programista, aby ponownie zgłosić dalej ten sam wyjątek (ang. rethrow exception), zapewne napisze tak (zgodnie ze swoją intuicją):

private static void B()
{
    try
    {
        C();
    }
    catch (Exception e)
    {
        throw e;
    }
}


CLR w momencie, gdy zgłaszamy wyjątek pisząc throw new Exception() zapisuje ślad stosu wywołań funkcji (ang. stack trace) - można się do niego dostać za pomocą właściwości StackTrace. Funkcja, w której wyjątek został zgłoszony jest jego źródłem. Niestety ku zaskoczeniu wielu programistów (mojemu też, gdy się o tym dowiedziałem) throw e;, co prawda rzuca ten sam wyjątek, który został złapany, ale niestety ślad stosu zostaje nadpisany nowym śladem ze źródłem w miejscu tego wywołania :(. To nieuntuicyjne zachowanie jest przyczyną późniejszych problemów podczas debugowania ponieważ nie znane jest rzeczywiste źródło błędu. Dlatego należy pamiętać aby nigdy nie rzucać wyjątków w ten sposób!

Przykładowo rozważmy następujący program:

static void Main(string[] args)
{
    int w = 1;
    A();
}
private static void A()
{
    int w = 2;
    B();
}
private static void B()
{
    try
    {
        int w = 3;
        C();
    }
    catch (Exception e)
    {
        throw e;
    }
}
private static void C()
{
    int w = 4;
    D();
}
private static void D()
{
    int w = 5;
    throw new Exception("The method or operation is not implemented.");
}


W wyniku jego uruchomienia dostaniemy następującą informację:

Wyjątek nieobsłużony: System.Exception: The method or operation is not implemented.
   w ConsoleApplication1.Program.B() w D:\ConsoleApplication1\ConsoleApplication1\Program.cs:wiersz 30
   w ConsoleApplication1.Program.A() w D:\ConsoleApplication1\ConsoleApplication1\Program.cs:wiersz 18
   w ConsoleApplication1.Program.Main(String[] args) w D:\ConsoleApplication1\ConsoleApplication1\Program.cs:wiersz 12

Rzeczywiście, niepoprawne ponowne rzucenie wyjątku w metodzie B() skutkuje obcięciem śladu stosu. Zatem jak sobie z tym poradzić? Do tego właśnie służy użycie throw; bez parametru. Jeżeli teraz zmodyfikujemy odpowiednio metodę B() to sytuacja się odmieni.

private static void B()
{
    try
    {
        C();
    }
    catch (Exception e)
    {
        throw;
    }
}


Wynik uruchomienia jest następujący:

Wyjątek nieobsłużony: System.Exception: The method or operation is not implemented.
   w ConsoleApplication1.Program.D() w D:\ConsoleApplication1\ConsoleApplication1\Program.cs:wiersz 43
   w ConsoleApplication1.Program.C() w D:\ConsoleApplication1\ConsoleApplication1\Program.cs:wiersz 37
   w ConsoleApplication1.Program.B() w D:\ConsoleApplication1\ConsoleApplication1\Program.cs:wiersz 30
   w ConsoleApplication1.Program.A() w D:\ConsoleApplication1\ConsoleApplication1\Program.cs:wiersz 18
   w ConsoleApplication1.Program.Main(String[] args) w D:\ConsoleApplication1\ConsoleApplication1\Program.cs:wiersz 12

Widać zatem, że ślad stosu nie został obcięty i łatwo odnaleźć prawdziwe źródło błędu. Myślę, że nie trzeba nikogo zachęcać do takiego właśnie obsługiwania wyjątków. To może nam tylko usprawnić pracę. Różnicę, o której tu mowa, najlepiej widać na przykładzie kodu MSIL. Dla pierwszego przypadku mamy:

L_000f: ldloc.1
L_0010: throw


Natomiast dla drugiego:

L_000e: nop
L_000f: rethrow


Jak łatwo zauważyć MSIL ma dwie osobne instrukcje. Jedna - throw - służy do zgłaszania wyjątków a druga - rethrow - do ponownego zgłaszania wyjątków. Twórcy C# skondensowali je jednak do jednego słowa kluczowego, co czasami rodzi problemy.

Chciałbym jeszcze zauważyć jedną bardzo ciekawą rzecz. Co prawda wywołanie throw; zachowuje ślad stosu to jednak ciekawy jest sposób w jaki to robi. Otóż z powyższych rozważań można by było wywnioskować, że powyższe wywołanie zachwuje się tak jakby wyjątek w ogóle nie został złapany. Otóż nie! Wyjątek ten został złapany a co więcej ma to swoje odbicie w śladzie stosu. Tak naprawdę to sam ślad stosu jest obcinany a zachowywana jest tylko jego pełna tekstowa reprezentacja (sic!). Zatem to co widzieliśmy w powyższych przykładach jest tylko zapisem śladu stosu. Tę sytuację najlepiej zobaczyć uruchamiając dwie powyższe aplikacje w trybie debug.

Wyjątek po uruchomieniu pierwszej wersji:


Wyjątek po uruchomieniu drugiej wersji:


Na załączonych obrazkach widać, że używając throw; rzeczywiście zachowujemy ślad stosu, ale gdy spojrzymy na okno rzeczywistego śladu stosu to okaże się, że kończy się on na metodzie, która złapała i ponownie rzuciła ten sam wyjątek. Zatem nie mamy możliwości pójścia w górę stosu i dotarcia do rzeczywistego źródła wyjątku. Nie mamy też dostępu do żadnych informacji kontekstowych takich jak zmienne i parametry tych wywołań, które zostały ucięte.

Można się zastanawiać jak poprawnie obsługiwać wyjątki. Ja myślę, że:
  • Przede wszystkim należy używać throw; zamiast throw e;
  • Po drugie jeżeli nie obsługujemy wyjątku to nie ma sensu stosować bloku try-catch i ponownie rzucać tego samego wyjątku bez żadnej obsługi.
Trzymając się tych wytycznych debugowanie będzie przyjemniejsze.
Opublikowane 9 października 2007 22:10 przez nuwanda
Filed under: ,

Powiadamianie o komentarzach

Jeżeli chciałbyś otrzymywać email gdy ta wypowiedź zostanie zaktualizowana, to zarejestruj się tutaj

Subskrybuj komentarze za pomocą RSS

Komentarze:

# re: C#: throw; vs throw e;

10 października 2007 14:43 by windywinter

Z przydatnych rzeczy z wyjatkami to [DebuggerStepThrough] oraz ustawienie VS na break when an ex is thrown dla CLR.

jesli mam pomocnicze metody rodzaju

static class Guard {

 static Fail(...)

 static AreEqual(...)

 static IsNotNull(...)

}

i uzycie:

void DoIt(a, b, c) {

 Guard.IsNotNull(a, "...");

 switch (b) {

   case "...": ...

   case "...": ...

   case "...": ...

   default:

     Guard.Fail("...");

 }

}

to w wersji pierszej jak "padnie" podczas debugowania to znajdziemy sie wewnatrz metody IsNotNull czy Fail tam gdize jest throw.

static class Guard {

 [DebuggerStepThrough]

 static Fail(...)

 [DebuggerStepThrough]

 static AreEqual(...)

 [DebuggerStepThrough]

 static IsNotNull(...)

}

A jesli damy atrybut [DebuggerStepThrough] to odwinie stos do miejsca wywolania IsNotNull czy Fail.

a drugi tip to menu Debug->Exceptions... i zaznaczenie checkboxa CLR Exceptions w thrown.

# re: C#: throw; vs throw e;

19 czerwca 2008 11:19 by adderek

A co z:

try {

C();

} catch (System.Exception e) {

throw new System.Exception("Cokolwiek", e);

}

W każdym razie wszystkie opcje są lepsze niż to, z czym się teraz spotykam:

try {

C();

} catch (System.Exception e) {

String errMsg; // NEVER DO THIS

errMsg = e.Message; // NIGDY TAK NIE RÓB

}

Co o tym myślisz?

(wymagane) 
wymagane 
(wymagane) 

  
Wprowadź kod: (wymagane)