Anonimowe metody w języku C#

Anonimowe metody (tzw. anonimowe delegaty) działają częściowo prawie jak domknięcia w językach funkcyjnych.

Czym są domknięcia?

Najbardziej przejrzysta definicja domknięcia w języku programowania, z jaką się spotkałem wymienia trzy cechy, które musi spełniać blok kodu, by stał się domknięciem:

  • może być przekazywany między kontekstami wykonania,
  • może być wykonany na żądanie w kontekście, w którym aktualnie się znajduje,
  • może odnosić się do zmiennych z kontekstów wykonania, w których został utworzony ("zamyka" te zmienne).

Anonimowe delegaty jako domknięcia.

Anonimowy delegat w języku C# nie jest domknięciem (dyskusja: http://blogs.msdn.com/abhinaba/archive/2005/10/18/482180.aspx), ale zachowuje niektóre zalety domknięć. Zatem, podobnie jak w domknięciach, jak najbardziej legalny jest tego typu kod:

//zdefiniowanie typu wywołania zwrotnego jako metody z jednym parametrem typu uogólnionego
public delegate void InstanceCallback<T>(T instance);
 
public class AnonymousDelegateTest
{    
  //Definicja metody, która wykonuje własną logikę i w międzyczasie przekazaną metodą wywołania zwrotnego
  public static void Do(InstanceCallback<int> blockHandlingInt)
  {
    //jakaś własna logika
    blockHandlingInt.Invoke(12);
    //jakaś własna logika
  }
 
  //jak do tej pory nic się nie dzieje - zastosowałem standardowy mechanizm wywołań
  //zwrotnych znany z czasów języka C.
 
  public static void Main(string[] args)
  {
 
    int lolek = 0;
 
    //wywołanie metody i przekazanie jej anonimowego delegata, który zostanie wykonany 
    //z argumentem number równym 12.
    Do(delegate(int number)
    {
      lolek = number; //przypisanie do zmiennej spoza zasięgu metody!
    });
 
    System.Console.WriteLine(lolek); // => 12
  }
}

W kodzie tym najważniejsza jest zmienna lolek, która, jak widać, jest współdzielona pomiędzy dwoma kontekstami wykonania i zachowuje pomiędzy nimi swój stan. Widać zatem, że korzystając z anonimowych delegatów i uogólnień oferowanych przez C# można osiągnąć pewien idiom, który jest nagminnie w użyciu w języku Ruby.

Jeśli się uważniej przyjrzeć temu mechanizmowi, to od razu nasuwa się skojarzenie z konstrukcję using dostępną w C#. Faktycznie, idiom ten można traktować jako sposób na osiągnięcie bardziej ogólnego i elastycznego zachowania na wzór using.

Przykład wykorzystania.

Załóżmy, że nasza aplikacja wykorzystuje połączenia do bazy danych i korzysta z podejścia, które polega na otwieraniu połączenia tylko wtedy, gdy chcemy wykonać jakieś komendy na bazie (w przeciwieństwie do podejścia z trzymaniem otwartego połączenia przez cały czas życia aplikacji). Oprócz tego chcemy, by każdy ciąg operacji na otwartym połączeniu był realizowany jako transakcja. Trzecim naszym wymaganiem jest, by każdy wyjątek był logowany. Po czwarte, każda operacja SQL wykonywana na bazie jest procedurą składowaną (i nie chce nam się za każdym razem tego pisać), a po piąte czas wygaśnięcia dla każdej operacji ma wynosić 500 (jest to odgórna polityka naszego projektu).

Mamy zatem następujące fragmenty funkcjonalności, które chcemy wyabstrahować:

  1. Otwieranie i zamykanie połączenia
  2. Rozpoczęcie i dopełnienie transakcji
  3. Zalogowanie w przypadku wyłapania wyjątku
  4. Ustawienie typu operacji na procedurę składowaną w obiekcie komendy SQL
  5. Ustawienie czasu wygaśnięcia operacji na 500

Wykorzystując zaprezentowany idiom, wykorzystanie napisanego kodu może wyglądać w ten sposób:

class MainObject{
  public static void Main(string[] args)
  {
    int rowCount = 0;
 
    // Cała zabawa:
    new OpenMyDBConnection().Do( delegate(SqlConnection conn)
    {
      new StoredProcedureCommand("GetRowsNumber", conn).Do( delegate(SqlCommand cmd)
      {
        cmd.Parameters.Add("@CategoryName", SqlDbType.VarChar, 80).Value = "toasters";
        cmd.Parameters.Add("@Ammount", SqlDbType.Int).Value = 11232;
        rowCount = (int)cmd.ExecuteScalar();
      });
    });
    // Koniec zabawy
 
    System.Console.WriteLine(rowCount);
  }
}

Powyższy kod został napisany w stylu jaki obowiązuje w Ruby. Gdyby chcieć napisać podobny kod w Ruby (zakładając na chwilkę, że API baz danych w Rubym jest analogiczne jak w C#, co oczywiście nie jest prawdą), wyglądałby on tak:

row_count = 0
 
# Cała zabawa
OpenMyDBConnection.new do |conn|
  StoredProcedureCommand.new("GetRowsNumber", conn) do |cmd|
    cmd.parameters.add("@CategoryName", :varchar, 80).value = "toasters"
    cmd.parameters.add("@Ammount", :int).value = 11232
    row_count = cmd.execute_scalar
  end
end
# Koniec zabawy
 
puts row_count

Składnia w Ruby jest bardziej naturalna, jednak poziom abstrakcji i wielokrotnej używalności w obu przypadkach jest podobny.

Wracają do kodu w C# - widać, że nie ma w nim żadnej z pięciu rzeczy, które chcieliśmy wyabstrahować. Gdzie zatem one są? Wystarczy spojrzeć na poniższy listing.

public delegate void InstanceCallback<T>(T instance);
 
public class OpenMyDBConnection
{
  public void Do(InstanceCallback<SqlConnection> callback)
  {
    try
    {
      // Rozpoczęcie transakcji:
      using(TransactionScope txScope = new TransactionScope())
      {
        using(SqlConnection conn 
          = new SqlConnection(@"Data Source=(local);Initial Catalog=myDataBase;Integrated Security=True;"))
        {
          // Otwieranie połączenia:
          conn.Open();
 
          //Wykonanie wstrzykniętego kodu przekazując mu obiekt połączenia:
          callback.Invoke(conn);
 
          // Dopełnienie transakcji:          
          txScope.Complete();
        } //Zamknięcie połączenia
      }
    }
    catch(Exception e)
    {
      // Zalogowanie w przypadku wyłapania wyjątku:
      System.Console.WriteLine("Problem while working with MyDB Database: " + e.ToString());
      throw e;
    }
  }
}
 
public class StoredProcedureCommand
{
  public StoredProcedureCommand(string procedureName, SqlConnection conn)
  {
    this.procedureName = procedureName;
    this.conn = conn;
  }
 
  public void Do(InstanceCallback<SqlCommand> cmdCallback)
  {
    try
    {
      using(SqlCommand cmd = new SqlCommand(procedureName, conn))
      {
        //Ustawienie typu operacji na procedurę składowaną w obiekcie komendy SQL:
        cmd.CommandType = System.Data.CommandType.StoredProcedure; 
 
        //Ustawienie czasu wygaśnięcia operacji na 500:
        cmd.CommandTimeout = 500;
 
        //Wykonanie wstrzykniętego kodu przekazując mu obiekt komendy:
        cmdCallback.Invoke(cmd); 
      }
    }
    catch(Exception e)
    {
      // Zalogowanie w przypadku wyłapania wyjątku:
      System.Console.WriteLine("Problem with SQL command: " + e.ToString());
      throw e;
    }
  }
 
  private string procedureName;
  private SqlConnection conn;
}

Podsumowanie

W tym krótkim wywodzie chciałem podkreślić dwie zasadnicze kwestie:

  1. Anonimowe delegaty mogą być bardzo przydatne, trzeba tylko mieć pomysł, jak je wykorzystać
  2. Nieograniczanie się do jednego języka albo do jednego rodzaju języków procentuje w postaci większej ilości ciekawych pomysłów. Wiedzą to twórcy C#, którzy przy każdej możliwej okazji czerpią z innych języków garściami.
O ile nie zaznaczono inaczej, treść tej strony objęta jest licencją Creative Commons Attribution-Share Alike 2.5 License.