To že, u konstruktu try-catch-finally v programovacím jazyce C# se blok finally provádí v případě, kdy vznikne při běhu kódu uvnitř try bloku výjimka, i v případě, kdy kód uvnitř try bloku proběhne korektně je skutečnost jistě velmi dobře známá. Existují ale nějaké případy, kdy se blok finally neprovádí?
Ano, určitě existují a lze jich najít asi hned několik, například jsou jimi některé výjimky, které vzniknou přímo v CLR jako například StackOverflowException. Při této výjimce je proces okamžitě ukončen bez jakékoliv šance nějak zasáhnout tj. nejsou spouštěny finally bloky, AppDomain.UnhandledException handler, CER (Constrained Execution Regions) bloky ani kritické finalizery.
Další takový případ, který si ukážeme, je možný ve spojení s chováním .NET aplikací při neošetřené výjimce. Mějme například následující konzolovou aplikaci:
public static void Main()
{
AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
try
{
Console.WriteLine("try 1:");
try
{
Console.WriteLine("\ttry 2:");
try
{
Console.WriteLine("\t\ttry 3:");
throw new Exception("Unhandled exception");
}
finally
{
Console.WriteLine("\t\tfinally block (3)");
}
}
catch
{
Console.WriteLine("\tcatch block (2)");
throw;
}
finally
{
Console.WriteLine("\tfinally block (2)"); //<--Nebude spuštěn
}
}
finally
{
Console.WriteLine("finally block (1)"); //<--Nebude spuštěn
}
}
static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
Console.WriteLine("CurrentDomain_UnhandledException: " + ((Exception)e.ExceptionObject).Message);
Environment.FailFast(null); //Zákaz spuštění finally bloků
}
Na úrovni aplikační domény zde máme zaregistrovanou událost UnhandledException. Tato událost dovoluje reagovat na neošetřenou výjimku před tím, než v jejím důsledku dojde k ukončení celého procesu (k tomu v tomto případě dojde, protože výjimka vzniká v hlavním vlákně, které je jediné vlákno na popředí). V tomto handleru jsou možné reakce na výjimku již ale poměrně omezené, například ukončení procesu již zabránit nelze. Co ale lze, je zakázat spuštění finally bloků, a to konkrétně zavoláním metody Environment.FailFast.
Výstup uvedeného příkladu po spuštění z příkazové řádky bude vypadat takto:
Je vidět, že ke spuštění finally bloků 2 a 1 opravdu nedošlo (u finally bloků 3 ano).
Proč tomu tak je?
V případě vyhození výjimky ve skutečnosti .NET neprovádí pouze jeden, ale dva nezávislé průchody volání na zásobníku. Při prvním průchodu se pouze zjišťuje, zda je daná výjimka ošetřená nebo nikoliv tj. hledá se nějaký catch blok odpovídající danému typu výjimky (v našem případě je nalezen catch blok 2). Pokud se najde, je teprve v druhém průchodu prováděn kód všech odpovídajících finally bloků (v našem případě jen finally blok 3) a nakonec kód původně nalezeného catch bloku. Při opakovaném vyhození výjimky uvnitř catch bloku je tento proces spuštěn znovu, ale až od tohoto místa opakovaného vyhození.
Pokud není v prvním průchodu žádný odpovídající catch blok nalezen, je výjimka považována za neošetřenou, druhý průchod je zatím odložen a runtime nejprve provádí další akce. První z nich je zobrazení WER (Windows Error Reporting) dialogu a druhou je spuštění UnhandledException handleru. Druhý průchod tj. spuštění ostatních finally bloků (v našem případě by se jednalo o finally blok 2 a 1) by byl proveden až po uzavření WER dialogu a pouze v případě, kdy by nebylo provedeno volání metody FailFast v handleru.
(Jako podklady pro tento příspěvek mi posloužili komentáře v diskuzi pod článkem zde.)