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:

  1. Otwieram using;
  2. Robię return, następuje skok GOTO do końca metody;
  3. 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 :)

1 KOMENTARZ

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