Login
IDisposable v příkladech

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.

Disposable instance jako lokální proměnná

Nesprávné řešeníNesprávné řešení

private static string ReadTextFile(string fileName)
{
	TextReader reader = new StreamReader(fileName);
	string content = reader.ReadToEnd();
	return content;
}

Code Analysis nám hlásí upozornění CA2000. Dispose nevoláme vůbec. Po zavolání metody ReadTextFile zůstane soubor nepřístupný.

Nesprávné řešeníNesprávné řešení

private static string ReadTextFile(string fileName)
{
	TextReader reader = new StreamReader(fileName);
	string content = reader.ReadToEnd();
	reader.Dispose();
	return content;
}

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ý.

Správné řešeníSprávné řešení

private static string ReadTextFile(string fileName)
{
	TextReader reader = new StreamReader(fileName);
	try
	{
		string content = reader.ReadToEnd();
		return content;
	}
	finally
	{
		reader.Dispose();
	}
}

Správné řešeníSprávné řešení (kratší varianta)

private static string ReadTextFile(string fileName)
{
	using (TextReader reader = new StreamReader(fileName))
	{
		string content = reader.ReadToEnd();
		return content;
	}
}

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.

Disposable instance jako návratová hodnota

Nesprávné řešeníNesprávné řešení

private static TextReader OpenTextFile(string fileName, int moveToLine)
{
	TextReader reader = new StreamReader(fileName);
	while (moveToLine > 0 && reader.ReadLine() != null)
	{
		moveToLine--;
	}
	return reader;
}

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ý.

Správné řešeníSprávné řešení

public static TextReader OpenTextFile(string fileName, int moveToLine)
{
	TextReader reader = new StreamReader(fileName);
	try
	{
		while (moveToLine > 0 && reader.ReadLine() != null)
		{
			moveToLine--;
		}
		return reader;
	}
	catch
	{
		reader.Dispose();
		throw;
	}
}

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.

Disposable instance jako člen jiné třídy

Nesprávné řešeníNesprávné řešení

public class TextFile
{
	private TextReader reader;

	public TextFile(string fileName, int moveToLine)
	{
		reader = new StreamReader(fileName);
		while (moveToLine > 0 && reader.ReadLine() != null)
		{
			moveToLine--;
		}
	}

	public string ReadLine()
	{
		string line = reader.ReadLine();
		return line;
	}
}

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.

Správné řešeníSprávné řešení

public class TextFile : IDisposable
{
	private TextReader reader = null;

	[SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
	public TextFile(string fileName, int moveToLine)
	{
		try
		{
			reader = new StreamReader(fileName);
			while (moveToLine > 0 && reader.ReadLine() != null)
			{
				moveToLine--;
			}
		}
		catch
		{
			Dispose();
			throw;
		}
	}

	~TextFile()
	{
		Dispose(false);
	}

	public void Dispose()
	{
		Dispose(true);
		GC.SuppressFinalize(this);
	}

	protected virtual void Dispose(bool disposing)
	{
		if (disposing)
		{
			if (reader != null)
			{
				reader.Dispose();
				reader = null;
			}
		}

		/*Zde bychom mohli uvolnit unmanaged zdroje,
		pokud by nejake nase trida primo alokovala.*/
	}

	public string ReadLine()
	{
		if (reader == null)
		{
			throw new ObjectDisposedException(null);
		}
		string line = reader.ReadLine();
		return line;
	}
}

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.

Disposable instance v kolekci

Nesprávné řešeníNesprávné řešení

private static void Add(this ICollection<TextReader> collection, string fileName, int moveToLine)
{
	TextReader reader = new StreamReader(fileName);
	while (moveToLine > 0 && reader.ReadLine() != null)
	{
		moveToLine--;
	}
	collection.Add(reader);
}

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ý.

Správné řešeníSprávné řešení

private static void Add(this ICollection<TextReader> collection, string fileName, int moveToLine)
{
	TextReader reader = new StreamReader(fileName);
	try
	{
		while (moveToLine > 0 && reader.ReadLine() != null)
		{
			moveToLine--;
		}
		collection.Add(reader);
	}
	catch
	{
		reader.Dispose();
		throw;
	}
}

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.

Disposable instance jako parametr konstruktoru

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.

Nesprávné řešeníNesprávné řešení

using (Stream stream = new FileStream("Test.txt", FileMode.Open, FileAccess.Read))
{
	using (TextReader reader = new StreamReader(stream))
	{
		string content = reader.ReadToEnd();
		Console.WriteLine(content);
	}
}

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.

Správné řešeníSprávné řešení

public static void Main()
{
	Stream stream = new FileStream("Test.txt", FileMode.Open, FileAccess.Read);
	try
	{
		using (TextReader reader = new StreamReader(stream))
		{
			stream = null;
			string content = reader.ReadToEnd();
			Console.WriteLine(content);
		}
	}
	finally
	{
		if (stream != null)
		{
			stream.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:

  1. Na vícenásobné volání Dispose (CA2202) nás Code Analysis upozorní pouze pokud třída implementuje rozhraní IDisposable a má parametr konstruktoru typu Stream, StreamReader, StreamWriter a to i přesto, že třída nebude ve skutečnosti sama zajišťovat volání Dispose tohoto parametru. Například konstruktor třídy StreamReader a StreamWriter má ještě další parametr leaveOpen. Dispose pro vnořený Stream se nevolá pokud tento parameter nastavíme na true. V tomto případě musíme tedy použít výše uvedené “nesprávné” řešení a Code Analysis nám bude hlásit upozornění CA2202 i když k vícenásobnému volání Dispose vnořeného Streamu ve skutečnosti nedochází.
  2. A naopak, pokud parameter konstruktoru bude nějaký jiný typ než Stream, StreamReader, StreamWriter, ale bude to stále disposable typ, a volající sám nezajistí volání Dispose pro instanci tohoto parametru, upozorní nás Code Analysis na toto chybějící volání Dispose (CA2000) i přesto, že třída bude ve skutečnosti sama zajišťovat volání Dispose tohoto parametru. Například metoda Create třídy XmlReader a XmlWriter má parametr XmlReaderSettings resp. XmlWriterSettings s vlastností CloseInput resp. CloseOuptut. Dispose pro vnořený Stream se volá pouze pokud tuto vlastnost nastavíme na true. V tomto případě musíme tedy použít výše uvedené “správné” řešení a Code Analysis nám bude hlásit upozornění na chybějící volání Dispose (CA2000) i když třída ve skutečnosti sama zajišťuje volání Dispose vnořeného Streamu.

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ů:

Správné řešeníSprávné řešení

public static class Program
{
	public static void Main()
	{
		using (TextReader reader = new StreamReader("Test.txt"))
		{
			TextFile file = new TextFile(reader, 3);
			string line = file.ReadLine();
			while (line != null)
			{
				Console.WriteLine(line);
				line = file.ReadLine();
			}
		}
	}
}

public class TextFile
{
	private TextReader reader = null;

	public TextFile(TextReader reader, int moveToLine)
	{
		if (reader == null)
		{
			throw new ArgumentNullException("reader");
		}
		while (moveToLine > 0 && reader.ReadLine() != null)
		{
			moveToLine--;
		}
		this.reader = reader;
	}

	public string ReadLine()
	{
		string line = reader.ReadLine();
		return line;
	}
}

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.

Správné řešeníSprávné řešení

public static class Program
{
	public static void Main()
	{
		TextReader reader = new StreamReader("Test.txt");
		try
		{
			using (TextFile file = new TextFile(reader, 3))
			{
				reader = null;
				string line = file.ReadLine();
				while (line != null)
				{
					Console.WriteLine(line);
					line = file.ReadLine();
				}
			}
		}
		finally
		{
			if (reader != null)
			{
				reader.Dispose();
			}
		}
	}
}

public class TextFile : IDisposable
{
	private TextReader reader = null;

	[SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
	public TextFile(TextReader reader, int moveToLine)
	{
		try
		{
			if (reader == null)
			{
				throw new ArgumentNullException("reader");
			}
			while (moveToLine > 0 && reader.ReadLine() != null)
			{
				moveToLine--;
			}
			this.reader = reader;//musi byt na konci, aby nedoslo k uvolneni readeru, pokud bude vyvolana vyjimka
		}
		catch
		{
			Dispose();
			throw;
		}
	}

	~TextFile()
	{
		Dispose(false);
	}

	public void Dispose()
	{
		Dispose(true);
		GC.SuppressFinalize(this);
	}

	protected virtual void Dispose(bool disposing)
	{
		if (disposing)
		{
			if (reader != null)
			{
				reader.Dispose();
				reader = null;
			}
		}
	}

	public string ReadLine()
	{
		if (reader == null)
		{
			throw new ObjectDisposedException(null);
		}
		string line = reader.ReadLine();
		return line;
	}
}

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.

Správné řešeníSprávné řešení

public static class Program
{
	public static void Main()
	{
		ReadWithoutLeaveOpen();
		ReadWithLeaveOpen();
	}

	public static void ReadWithoutLeaveOpen()
	{
		TextReader reader = new StreamReader("Test.txt");
		try
		{
			using (TextFile file = new TextFile(reader, 3, false))
			{
				reader = null;
				string line = file.ReadLine();
				while (line != null)
				{
					Console.WriteLine(line);
					line = file.ReadLine();
				}
			}
		}
		finally
		{
			if (reader != null)
			{
				reader.Dispose();
			}
		}
	}

	[SuppressMessage("Microsoft.Usage", "CA2202:Do not dispose objects multiple times")]
	public static void ReadWithLeaveOpen()
	{
		using (TextReader reader = new StreamReader("Test.txt"))
		{
			using (TextFile file = new TextFile(reader, 3, true))
			{
				string line = file.ReadLine();
				while (line != null)
				{
					Console.WriteLine(line);
					line = file.ReadLine();
				}
			}
		}
	}
}

public class TextFile : IDisposable
{
	private TextReader reader = null;
	private bool leaveOpen = false;

	[SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
	public TextFile(TextReader reader, int moveToLine, bool leaveOpen)
	{
		try
		{
			if (reader == null)
			{
				throw new ArgumentNullException("reader");
			}
			while (moveToLine > 0 && reader.ReadLine() != null)
			{
				moveToLine--;
			}
			this.reader = reader;//musi byt na konci, aby nedoslo k uvolneni readeru, pokud bude vyvolana vyjimka
			this.leaveOpen = leaveOpen;//musi byt na konci, aby nedoslo k uvolneni readeru, pokud bude vyvolana vyjimka
		}
		catch
		{
			Dispose();
			throw;
		}
	}

	~TextFile()
	{
		Dispose(false);
	}

	public void Dispose()
	{
		Dispose(true);
		GC.SuppressFinalize(this);
	}

	protected virtual void Dispose(bool disposing)
	{
		if (disposing)
		{
			if (reader != null)
			{
				if (leaveOpen)
				{
					reader.Dispose();
				}
				reader = null;
			}
		}
	}

	public string ReadLine()
	{
		if (reader == null)
		{
			throw new ObjectDisposedException(null);
		}
		string line = reader.ReadLine();
		return line;
	}
}

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ů.

Number of comments: 0