Każdy z nas uczy się cały życie, ja zaś nauczyłem się czegoś dzisiaj ;) było to na tyle ciekawe dla mnie, że stwierdziłem iż to opiszę :) A morał całej historii podam na końcu ;)
Większość z nas zna wyrażenie w C# using, które powoduje wywołanie metody Dispose na obiekcie implementującym interfejs IDisposable.
Przykład prostego wykorzystania using, podczas tworzenia pliku i go odczytywania:
class Program { static void Main(string[] args) { using(TextWriter w = File.CreateText("log.txt")) { w.WriteLine("This is line one"); w.WriteLine("This is line two"); } using(TextReader r = File.OpenText("log.txt")) { string s; while((s = r.ReadLine()) != null) { Console.WriteLine(s); } } } }
W kilku prostych słowach, to metoda Dispose naszych TextWriter i TextReader zostanie wywołana tuż przy wyjście z bloku using. Jednak co kiedy rozbudujemy nasz kod o dodatkową klasę która będzie miała dwie metody – void CreateFile, string ReadFile?
class Program { static void Main(string[] args) { var test = new FileTest(); test.CreateFile("log.txt"); Console.WriteLine(test.ReadFile("log.txt")); } } class FileTest { public void CreateFile(string name) { using(TextWriter w = File.CreateText("log.txt")) { w.WriteLine("This is line one"); w.WriteLine("This is line two"); } } public string ReadFile(string name) { using(TextReader r = File.OpenText("log.txt")) { string s; string result = string.Empty; while((s = r.ReadLine()) != null) { result += s; } if(!string.IsNullOrEmpty(result)) { return result; } } return "File is empty"; } }
Czy jeżeli wykorzystujemy polecenie return w wyrażeniu using to dalej wywoływany jest Dispose?
Aż wstyd się przyznać, osobiście sądziłem, że nie. Może jest to spowodowane moim zboczeniem na punkcie SharePoint, gdzie zależności do obiektów implementujących IDisposable jest poukrywana – to prawie jak zabawa w chowanego, czy aby na pewno ten obiekt nie ma schowanej instancji SPWeb? ;)
Dlaczego tak sądziłem? return jest ostatnim wyrażeniem w metodzie, jest on zawsze nawet jak mamy void, to w IL będzie wykorzystany return. Więc patrząc z chłopskiego punktu widzenia:
- Otwieram using;
- Robię return, następuje skok GOTO do końca metody;
- Zamykam using – ale jak kod ma zostać wykonany skoro już z niego wyskoczyliśmy w punkcie 2?
Rozwiązanie tego wszystkiego leży w definicji wyrażenia using, które tak naprawdę jest syntatic sugar na blok try{} finally{}. Można to zauważyć oglądając kod w ILDasm (w .NET Reflectorze jest to trochę inaczej pokazane, i na pewno trudniej będzie to zrozumieć), poniżej jest przykład prosto z metody CreateFile:
.method public hidebysig instance void CreateFile(string name) cil managed { // Code size 58 (0x3a) .maxstack 2 .locals init ([0] class [mscorlib]System.IO.TextWriter w, [1] bool CS$4$0000) IL_0000: nop IL_0001: ldstr "log.txt" IL_0006: call class [mscorlib]System.IO.StreamWriter [mscorlib]System.IO.File::CreateText(string) IL_000b: stloc.0 .try { IL_000c: nop IL_000d: ldloc.0 IL_000e: ldstr "This is line one" IL_0013: callvirt instance void [mscorlib]System.IO.TextWriter::WriteLine(string) IL_0018: nop IL_0019: ldloc.0 IL_001a: ldstr "This is line two" IL_001f: callvirt instance void [mscorlib]System.IO.TextWriter::WriteLine(string) IL_0024: nop IL_0025: nop IL_0026: leave.s IL_0038 } // end .try finally { IL_0028: ldloc.0 IL_0029: ldnull IL_002a: ceq IL_002c: stloc.1 IL_002d: ldloc.1 IL_002e: brtrue.s IL_0037 IL_0030: ldloc.0 IL_0031: callvirt instance void [mscorlib]System.IDisposable::Dispose() IL_0036: nop IL_0037: endfinally } // end handler IL_0038: nop IL_0039: ret } // end of method FileTest::CreateFil
Przypatrzmy się więc temu co widzimy :) Nasze wyrażenie:
using(TextWriter w = File.CreateText("log.txt")) { w.WriteLine("This is line one"); w.WriteLine("This is line two"); }
Zostało zamienione na (żeby się już ILem na razie nie posługiwać napiszę to samo w C#):
TextWriter w = File.CreateText("log.txt");
try { w.WriteLine("This is line one"); w.WriteLine("This is line two"); } finally { ((IDisposable) w).Dispose(); }
Czyli formalizując:
using ( expression ) embedded-statement
Zostanie zamienione na:
ResourceType resource = expression; try { embedded-statement } finally { … // Dispose of resource }
Gdzie, expression jest równy ResourceType resource == expression i może być zamiennie stosowany w using.
Dodatkowo, resource jest traktowane jako zmienna tyko do odczytu w bloku embedded-statement – o tym trzeba pamiętać.
Zaś finally w zależności od tego czy ResourceType jest nullable czy też nie będzie wyglądał następująco:
W przypadku nullable:
finally { if(resource != null) ((IDisposable)resource).Dispose(); }
W przypadku kiedy nie jest nullable:
finally { ((IDisposable)resource).Dispose(); }
W każdym innym przypadku (czyli kiedy nie będzie można rozpoznać czy obiekt w jakiś sposób implementuje IDisposable) zostanie zwrócony błąd kompilacji:
‘ConsoleApplication4.Program’: type used in a using statement must be implicitly convertible to ‘System.IDisposable’ D:_ProjectsConsoleApplication4ConsoleApplication4Program.cs
No dobrze, ale wracając do naszego przypadku kiedy następuje return w using.
W powyższym IL jest taka komenda tuż przed zamknięciem try:
IL_0026: leave.s IL_0038
Żeby nie układać w swoje słowa tego co jest napisane o to opis do czego służy leave:
The leave instruction is similar to the br instruction, but the former can be used to exit a try,filter, or catch block whereas the ordinary branch instructions can only be used in such a block to transfer control within it. The leave instruction empties the evaluation stack and ensures that the appropriate surrounding finally blocks are executed.
It is not valid to use a leave instruction to exit a finally block. To ease code generation for exception handlers it is valid from within a catch block to use a leave instruction to transfer control to any instruction within the associated try block.
Czyli mówiąc po polsku, leave służy do tego by wyjść z bloku try, tak by finally zostało wywołane.
W naszym przypadku, kiedy mamy return kod IL wygląda tak:
IL_0042: leave.s IL_0062 IL_0044: nop IL_0045: leave.s IL_0059
Gdzie, pierwszy leave odpowiada za zwrócenie przeczytanego pliku, zaś drugi do przejścia poza blok try{} finally{}.
Więc, niezależnie od tego czy mamy return w using czy go nie mamy, metoda Dispose zostanie wywołana :)
To tyle, mam nadzieję, że się to komuś przyda, tak jak mi się to przydało ;)
Zachęcam wszystkich do zapoznania się ze specyfikacją C# i/lub IL, albo zaglądania do nich w kwestiach wspornych :)
A więc pora na morał:
To co wygląda jak kaczka, nie zawsze kwacze :)
Jest jedna sytuacja, w której using nie zachowa się tak jak byśmy tego oczekiwali. Proponuję porównać jaki IL wygenerują poniższe fragmenty:
using(var connection = new SqlConnection(){ConnectionString = “conn”})
{
connection.Open();
}
using (var connection = new SqlConnection(“conn”))
{
connection.Open();
}
Comments are closed.