czwartek, 19 grudnia 2019

Rzucanie wyjątków wewnątrz bloku using

Jednym z zaleceń Microsoftu podczas implementacji interfejsu IDisposable jest aby implementowana metoda nie rzucała wyjątku https://docs.microsoft.com/en-us/visualstudio/code-quality/ca1065-do-not-raise-exceptions-in-unexpected-locations?view=vs-2015&redirectedfrom=MSDN

Oczywiście zalecenia zaleceniami, a życie życiem :) Jednym z koronnych przykładów złamania tego zalecenia jest klasa ChannelFactory<TChannel> która pozwala na komunikację z webserwisem WCF. Jeżeli spojrzymy do źródeł tej klasy zobaczymy wiele miejsc, w których metoda Dispose rzuca wyjątkami związanymi np. z połączeniem do serwisu. Dlaczego jest to dla nas takie istotne? 
Aby uświadomić sobie problem z którym mamy tutaj do czynienia spójrzmy na kawałek przykładowego kodu który implementuje interfejs IDisposable

    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                ShowSomethingOnScreen();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
                throw;
            }
        }

        private static void ShowSomethingOnScreen()
        {
            using (var dc = new DisposableClass())
            {
                dc.DoSomething();
                throw new InvalidOperationException("Using block");
            }
        }
    }

    public class DisposableClass : IDisposable
    {
        public void DoSomething()
        {
            Console.WriteLine("I'm doing some work!");
        }

        public void Dispose()
        {
            throw new InvalidOperationException("DisposeMethod");
        }
    }

Powyższy przykład rzuca dwa wyjątki:
  • jeden z wyjątków zdefiniowany jest w metodzie ShowSomethingOnScreen
  • rugi z wyjątków znajduje się w metodzie Dispose klasy DisposableClass
Zadajmy sobie pytanie: po uruchomieniu aplikacji, który z wyjątków zostanie przechwycony w metodzie Main i dlaczego? 
Na pierwszy rzut oka wydawałoby się, że zostanie przechwycony wyjątek z treścią "Using block". Stanie się jednak inaczej - zostanie 



Aby odpowiedź na pytanie dlaczego przechwycimy wyjątek z metody Dispose a nie z wewnątrz bloku using należy zobaczyć w jaki sposób kompilator przetłumaczy nasz kod. Blok using w rzeczywistości jest blokiem try finally:

 .method private hidebysig static 
  void ShowSomethingOnScreen () cil managed 
 {
  // Method begins at RVA 0x2084
  // Code size 37 (0x25)
  .maxstack 1
  .locals init (
   [0] class UsingKeyWord.DisposableClass dc
  )

  // (no C# code)
  IL_0000: nop
  // using (DisposableClass disposableClass = new DisposableClass())
  IL_0001: newobj instance void UsingKeyWord.DisposableClass::.ctor()
  // (no C# code)
  IL_0006: stloc.0
  .try
  {
   IL_0007: nop
   // disposableClass.DoSomething();
   IL_0008: ldloc.0
   IL_0009: callvirt instance void UsingKeyWord.DisposableClass::DoSomething()
   // (no C# code)
   IL_000e: nop
   // throw new InvalidOperationException("Using block");
   IL_000f: ldstr "Using block"
   IL_0014: newobj instance void [System.Runtime]System.InvalidOperationException::.ctor(string)
   // (no C# code)
   IL_0019: throw
  } // end .try
  finally
  {
   IL_001a: ldloc.0
   IL_001b: brfalse.s IL_0024

   IL_001d: ldloc.0
   IL_001e: callvirt instance void [System.Runtime]System.IDisposable::Dispose()
   IL_0023: nop

   IL_0024: endfinally
  } // end handler
 } // end of method Program::ShowSomethingOnScreen


Możemy więc rozpisać w jaki sposób zostanie przetworzony program:
  1. Program startuje od metody Main i wywołuje metodę ShowSomethingOnScreen w bliku try catch
  2. Otwarcie bolku using w metodzie ShowSomethingOnScreen 
  3. Wywołanie metody DoSomething która wypisuje zdanie w konsoli
  4. Rzucenie wyjątku throw new InvalidOperationException("Using block");
  5. Wywołanie bloku finally, który wywołuje metodę Dispose
  6. Metoda Dispose rzuca wyjątek
  7. Wyjątek rzucony w metodzie Dispose przekazywany jest do bloku catch metody Main
Teraz zostaje już tylko odpowiedzieć pytanie - jak prawidłowo obsłużyć oba wyjątki aby nie pominąć żadnego z nich?
Niestety, ale aby było możliwe przechwycenie obu wyjątków musimy porzucić blok using i skorzystać ze zwykłego bolku try catch finally:

    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                ShowSomethingOnScreen();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
                throw;
            }
        }

        private static void ShowSomethingOnScreen()
        {
            DisposableClass dc = null;
            try
            {
                dc = new DisposableClass();
                throw new InvalidOperationException("Using block");
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
            finally
            {
                dc?.Dispose();
            }
        }
    }

    public class DisposableClass : IDisposable
    {
        public void DoSomething()
        {
            Console.WriteLine("I'm doing some work!");
        }

        public void Dispose()
        {
            throw new InvalidOperationException("DisposeMethod");
        }
    }


Wydawałoby się, że problem raczej niezbyt popularnych, jednak można z nim się spotkać i warto wiedzieć skąd bierze się jego źródło :)

Brak komentarzy:

Prześlij komentarz