Rozhraní IDisposable slouží k uvolnění “unmanaged” zdrojů. Nejčastěji to jsou různé objekty z Win32API (otevřené soubory, síťové spojení, GDI objekty, paměťové bloky, atd.) Autor třídy nám implementací rozhraní IDisposable dává najevo, že je přinejmenším vhodné uvolnit její zdroje voláním metody Dispose co nejdříve jakmile je to možné a nenechat tuto odpovědnost až na garbage collectoru. Garbage collector je totiž optimalizovaný hlavně na paměťové zdroje, u kterých lze pozdržet uvolnění natolik, že to u jiných zdrojů může způsobit zablokování přístupu k těmto zdrojům na zbytečně dlouhou dobu (databázové transakce, otevřené soubory) nebo jejich fatální vyčerpání (GDI objekty).
K odhalení nesprávného použití disposable instancí v našem kódu, nám může pomoci nástroj Code Analysis z Visual Studia. Nejčastěji se asi v kódu setkáváme s upozorněními, které spadají pod pravidla CA1001, CA2000 a CA2202.
V příkladech níže si ukážeme jak správně pracovat s disposable instancemi tak, aby náš kód těmto pravidlům vyhovoval. Budeme používat třídu StreamReader, která implicitně po vytvoření instance (otevření souboru) soubor znepřístupní pro zápis dokud instanci neuvolníme voláním metody Dispose. Tuto třídu jsme vybrali pouze proto, že implementuje rozhraní IDisposable a alokuje “unmanaged” zdroj, který lze snadno zablokovat a jeho zablokování lze snadno prokázat. Vše co bude zmíněno, ale principiálně platí pro všechny třídy implementující rozhraní IDisposable.
Code Analysis nám hlásí upozornění CA2000. Dispose nevoláme vůbec. Po zavolání metody ReadTextFile zůstane soubor nepřístupný.
Code Analysis nám hlásí upozornění CA2000. Dispose sice voláme, ale pouze pokud v metodě nedojde k výjimce. Pokud dojde k výjimce po otevření souboru, volající ji někde dále ošetří a program bude pokračovat, zůstane soubor nepřístupný.
Nikdy se nespoléhejme na to, že na konkrétním řádku kódu nemůže dojít k výjimce. Může k ní dojít téměř všude i na tom nejlépe otestovaném fragmentu kódu. V tomto případě může podle dokumentace dojít například k výjimce OutOfMemoryException nebo IOException při volání metody ReadToEnd a viníkem vůbec nemusí být naše aplikace. Nebo stačí když bude tato metoda volaná v samostatném vlákně a dané vlákno se přeruší metodou Thread.Abort. Kdekoliv v této metodě pak může dojít k výjimce ThreadAbortException a pokud tuto situaci správně neošetříme, zůstane soubor nepřístupný.
Code Analysis nám nehlásí žádné upozornění. Dispose voláme za každých okolností. Soubor zůstane přístupný po každém zavolání naší metody ať dopadne úspěšně nebo dojde k výjimce.
Code Analysis nám hlásí upozornění CA2000. Dispose ale volat nemůžeme, protože instanci předáváme jako návratovou hodnotu a to znamená, že předáváme i odpovědnost za volání Dispose na volajícího. Upozornění, které v tomto případě hlásí Code Analysis, se ale vztahuje pouze na situaci, kdy v metodě dojde k výjimce. Instance může být vytvořena, ale z důvodu pozdější výjimky nebude předána volajícímu a nebude tak ani zavolán Dispose a soubor zůstane neřístupný.
Code Analysis nám nehlásí žádné upozornění. Dispose voláme pouze pokud dojde k výjimce a výjimku poté předáme dále. V případě úspěchu je delegována odpovědnost za volání Dispose na volajícího. Code Analysis bude správně volajícímu hlásit upozornění CA2000, pokud nebude volat Dispose. Musíme ale zvolit název metody s prefixem Open nebo Create, abychom dali najevo Code Analysis, ale hlavně volajícímu kodérovi, že metoda vrací pokaždé novou instanci a deleguje na volajícího odpovědnost za volání Dispose. Nikde v dokumentaci jsem nenašel úplný výčet prefixů, u kterých Code Analysis předpokládá tuto delegaci, takže zmíněný výčet prefixů je pouze výsledkem krátké zkoušky. Metoda ale může vracet i cachovanou instanci a v tom případě by měla být za volání Dispose odpovědná samotná cache a volající naopak Dispose volat nesmí. V případě cachované instance musíme tedy zvolit například prefix Get. Code Analysis pak nebude požadovat volání Dispose po volajícím.
Code Analysis nám hlásí upozornění CA1001. Dispose nevoláme vůbec. Po opuštění bloku kódu, který omezuje rozsah působnosti instance naší třídy, zůstane soubor nepřístupný. Otázka ale je, kam v naší třídě umístit volání reader.Dispose ? Odpověď najdeme v dokumentaci k upozornění CA1001, která nám říká, že pokud třída vytváří a drží instance jiných tříd, které implementují rozhraní IDisposable, musí toto rozhraní implementovat i tato třída.
Volání Dispose v konstruktoru výše uvedeného příkladu je důležité v situaci, kdy v konstruktoru dojde k výjimce. Pokud totiž dojde k výjimce v konstruktoru, tak volající nezíská žádnou instanci a tudíž nemůže volat ani Dispose. Pokud v našem konstruktoru dojde k výjimce po otevření souboru, volající ji někde dále ošetří a program bude pokračovat, zůstane soubor nepřístupný. Naše třída také může být potomkem jiné třídy, která implementuje rozhraní IDisposable a její konstruktor, který se volá před naším konstruktorem, může také alokovat nějaké zdroje. Pokud bychom v našem konstruktoru nevolali Dispose, tak by se tyto zdroje, v případě výjimky v našem konstruktoru, také neuvolnily.
Jenže, správně naimplementovaná metoda Dispose volá další virtuální metodu Dispose(bool) a v konstruktoru bychom neměli volat žádné virtuální metody. Code Analysis nám toto hlásí jako upozornění CA2214. Účelem tohoto pravidla je zabránit volání kódu potomka, u kterého ještě nebyl zavolán jeho konstruktor, protože to obvykle nedopadne dobře. Metoda Dispose, ale musí být vždy naimplementována robustně. Neměla by způsobit žádný problém ani když ji zavoláme opakovaně na již uvolněné instanci. Neměla by tedy způsobit žádný problém ani když ji zavoláme na potomkovi, u kterého ještě nebyl zavolán konstruktor. Code Analysis by nemusel být v případě volání Dispose z konstruktoru tak důsledný, ale bohužel je, takže nám nezbývá nic jiného než toto upozornění, potlačit atributem SuppressMessage nad naším konstruktorem.
Metoda Dispose(bool) je virtuální, aby mohl i případný potomek naší třídy, uvolnit svoje zdroje, které alokuje.
Parametr disposing v metodě Dispose(bool) indikuje, zda uvolnění zdrojů požadujeme sami voláním Dispose nebo zda toto provádí až garbage collector prostřednictvím finalizeru. Pokud totiž zapomeneme uvolnit zdroje sami a necháme to až na garbage collectoru, mohou již být uvolněny všechny ostatní zdroje, kromě “unmanaged” zdrojů alokovaných přímo naší třídou. V této fázi bychom tedy měli zajistit uvolnění pouze těchto zbývajících zdrojů, jinak riskujeme výjimku. V naší třídě žádné přímo alokované “unmanaged” zdroje nejsou, máme alokované pouze “unmanaged” zdroje prostřednictvím další vnitřní instance typu StreamReader. Ale pokud bychom je měli, tak bychom je měli uvolnit v místě, kde je to v příkladu naznačeno komentářem.
Voláním GC.SuppressFinalize sdělíme garbage collectoru, že již nemá volat finalizer naší třídy, protože jsme zajistili uvolnění všech jejich zdrojů sami.
Po zavolání Dispose by mělo při každém dalším volání naší třídy dojít k výjimce ObjectDisposedException, jak můžeme vidět v metodě ReadLine. Netýká se to ale metody Dispose, jak již bylo řečeno dříve, ta musí být robustní v jakémkoliv stavu instance.
Code Analysis nám hlásí upozornění CA2000. Dispose ale volat nemůžeme, protože instanci ukládáme do kolekce a to znamená, že předáváme i odpovědnost za volání Dispose na jinou část kódu. Upozornění, které v tomto případě hlásí Code Analysis, se ale vztahuje pouze na situaci, kdy v metodě dojde k výjimce. Instance může být vytvořena, ale z důvodu pozdější výjimky nebude přidána do kolekce a nebude tak ani zavolán Dispose a soubor zůstane neřístupný.
Code Analysis nám nehlásí žádné upozornění. Dispose voláme pouze pokud dojde k výjimce a výjimku poté předáme dále. V případě úspěchu je delegována odpovědnost za volání Dispose na jinou část kódu, ale na volání Dispose v této jiné části už musíme myslet sami, protože tak intelgentní už Code Analysis není.
Takto se Code Analysis chová pouze v případě uložení disposable instance pomocí metod z rozhraní ICollection. Pokud si napíšeme nějakou vlastní metodu, která si předanou disposable instanci někam uloží, a volající této metody nebude tedy volat Dispose, tak nás bude Code Analysis stále upozorňovat na toto chybějící volání Dispose a budeme toto upozornění muset potlačit atributem SuppressMessage.
V tomto případě je důležité nejprve zmínit další pravidlo Code Analysis. Přestože metoda Dispose nesmí selhat i když bude volána opakovaně pro stejnou instanci, existuje pravidlo CA2202, které na toto vícenásobné volání upozorňuje a správně bychom se tomu měli vyhnout. Typický příklad je použití konstruktoru StreamReader, do kterého vstupuje jako parametr Stream.
Code Analysis nám hlásí upozornění CA2202. StreamReader implicitně v metodě Dispose volá i metodu Dispose pro Stream, který mu přadáváme v konstruktoru. To znamená, že první klíčové slovo using pro Stream způsobuje vícenásobné volání metody Dispose.
Code Analysis nám nehlásí žádné upozornění. Ihned po úspěšném volání konstruktoru StreamReader, nastavujeme proměnnou stream na null, aby v bloku finally nedošlo k vícenásobnému volání Dispose. Pokud by došlo k výjimce po vytvoření instance Stream, ale ještě před vytvořením instance StreamReader, musíme pro instanci Stream volat Dispose v bloku finally, jinak soubor zůstane nepřístupný.
Takže zde máme dvě hodně související pravidla: CA2000 vyžaduje volání Dispose alespoň jednou a CA2202 vyžaduje volání Dispose pouze jednou. Code Analysis ale tato pravidla nevyhodnocuje nijak sofistikovaně a často pak dochází k těmto dvěma konfliktům:
Pro disposable instanci jako parametr konstruktoru neexistuje žádné jednoznačné doporučení v tom, která část kódu, by měla být odpovědná za volání Dispose. Dokonce i v .NET Frameworku najdeme třídy, které toto řeší různě. Níže jsou tedy příklady několika možných způsobů:
Třída nezajišťuje volání Dispose pro disposable instanci předanou v konstruktoru. Odpovědnost za volání Dispose pro disposable instanci předanou v konstruktoru zůstává po celou dobu na volajícím. Třída nemusí implementovat rozhraní IDisposable.
Třída zajišťuje volání Dispose pro disposable instanci předanou v konstruktoru. Dokud nebude konstruktor třídy úspěšně zavolán zůstává odpovědnost za volání Dispose na volajícím. Jakmile bude konstruktor úspěšně zavolán přebírá tuto odpovědnost třída, kterou jsme tímto konstruktorem instanciovali.
Třída zajišťuje volání Dispose pro disposable instanci předanou v konstruktoru volitelně na základě dalšího parametru. Parametrem lze volit jedno ze dvou předchozích způsobů.