Wyrażenia lambda w praktyce - MethodWrappers

Kiedyś już wspominałem o fajnym wykorzystaniu wyrażeń lambda w poście Wyrażenia lambda i extension methods - aspektejszyn. Dzisiaj przytoczę kolejne przykłady takiego ich zastosowania, które potrafią znacząco ograniczyć ilość powtarzalnego kodu w kodzie (badaniem ilości cukru w cukrze zajął się kto inny).

Całość wrzuciłem sobie do statycznej klasy MethodWrappers, przyjrzyjmy się jej zawartości...

IgnoreExceptions()

Celem tej metody jest maksymalne skrócenie takiego potwora:

  1:  try
  2:  {
  3:  	CanThrow();
  4:  }
  5:  catch
  6:  {
  7:  }
  8:  try
  9:  {
 10:  	WillPossiblyThrowException();
 11:  }
 12:  catch
 13:  {
 14:  
 15:  }
 16:  try
 17:  {
 18:  	ShouldNotThrowButWhoKnows();
 19:  }
 20:  catch
 21:  {
 22:  
 23:  }

Chcemy, aby wykonały się WSZYSTKIE metody, niezależnie od wyrzucanych wyjątków, które zignorujemy. Tak, wiem, łykanie wszystkiego w try/catch jest praktyką NIEPOLECANĄ, jednak skądś takie brzydale znamy, prawda?

Pierwszy upraszczacz skraca kod do:

  1:  MethodWrappers.IgnoreExceptions(
  2:  	CanThrow,
  3:  	WillPossiblyThrowException,
  4:  	ShouldNotThrowButWhoKnows
  5:  	);

Prawda że ładniej? Oto kod podspodowy:

  1:  /// <summary>
  2:  /// Executes given operations catching all exceptions.
  3:  /// Exceptions thrown by the operations are ignored and do not bubble up to the caller.
  4:  /// Exceptions do not prevent other operations from being executed.
  5:  /// </summary>
  6:  public static void IgnoreExceptions(params Action[] operations)
  7:  {
  8:  	foreach (var operation in operations)
  9:  	{
 10:  		try
 11:  		{
 12:  			operation();
 13:  		}
 14:  		catch
 15:  		{
 16:  		}
 17:  	}
 18:  }

ExecuteTryCatch()

Druga z metod również tyczy się wyjątków. Ona z kolei ma skrócić taki kod:

  1:  try
  2:  {
  3:  	CanThrow();
  4:  }
  5:  catch
  6:  {
  7:  	Hilfe();
  8:  }
  9:  try
 10:  {
 11:  	WillPossiblyThrowException();
 12:  }
 13:  catch
 14:  {
 15:  	Rollback();
 16:  }
 17:  try
 18:  {
 19:  	ShouldNotThrowButWhoKnows();
 20:  }
 21:  catch
 22:  {
 23:  	Log();
 24:  }

W efekcie uzyskamy tylko 3 linijki - pozbywamy się ohydnej "rozwlekłości" nie tracąc jednocześnie czytelności! (z tą czytelnością pewnie nie wszyscy się zgodzą, ale... to kwestia przyzwyczajenia do lamd, zawsze można wstawić dodatkowy ENTER tu czy tam):

  1:  MethodWrappers.ExecuteTryCatch(CanThrow, Hilfe);
  2:  MethodWrappers.ExecuteTryCatch(WillPossiblyThrowException, Rollback);
  3:  MethodWrappers.ExecuteTryCatch(ShouldNotThrowButWhoKnows, Log);

Kod ową rozwlekłość w sobie bohatersko zatrzymujący:

  1:  /// <summary>
  2:  /// Executes the <paramref name="tryOperation"/> and performs the <paramref name="catchOperation"/> in case of exception.
  3:  /// </summary>
  4:  /// <param name="tryOperation">Operation to be executed.</param>
  5:  /// <param name="catchOperation">Operation to be executed in case of exception.</param>
  6:  public static void ExecuteTryCatch(Action tryOperation, Action catchOperation)
  7:  {
  8:  	try
  9:  	{
 10:  		tryOperation();
 11:  	}
 12:  
 13:  	catch
 14:  	{
 15:  		catchOperation();
 16:  	}
 17:  }

AggregateResults()

Kolejny kodoskracacz służy już do czegoś z wyjątkami niezwiązanego. Bardziej nawet kładę w nim nacisk na czytelność kodu niż na jego ilość. Zobaczmy PRZED:

  1:  List<int> positiveResults = new List<int>();
  2:  int temp = FirstOperationReturningInt();
  3:  if (temp >= 0)
  4:  	positiveResults.Add(temp);
  5:  temp = Sum(1, 2);
  6:  if (temp >= 0)
  7:  	positiveResults.Add(temp);

Cóż nas zatem interesuje? Chcemy mieć wyniki działania wszystkich metod zebrane w jednej kolekcji. ALE! Wyniki ujemne odrzucamy. Zbyt wiele razy miałem do czynienia z wyżej pokazanym kodem, czy nie ładniej tak?:

  1:  List<int> positiveResults = MethodWrappers.AggregateResults(
  2:  	number => number >= 0,
  3:  	FirstOperationReturningInt,
  4:  	() => Sum(1, 2)
  5:  	);

Najpierw definiujemy warunek, czyli JAKIE wartości mają być zapamiętywane, a potem listujemy operacje do wykonania. Nie wiem jak wam, ale mi się podoba. Kod:

  1:  /// <summary>
  2:  /// Executes each operation and creates an array of their results if.
  3:  /// Only results that satisfy a given condition are aggregated.
  4:  /// </summary>
  5:  /// <typeparam name="T">Type of a value returned by each of the actions.</typeparam>
  6:  /// <param name="condition">Condition that must be satisfied for the result to be aggregated.</param>
  7:  /// <param name="operations">Array of aggregated results of the operations.</param>
  8:  public static List<T> AggregateResults<T>(Predicate<T> condition, params Func<T>[] operations)
  9:  {
 10:  	List<T> results = new List<T>(operations.Length);
 11:  
 12:  	foreach (var operation in operations)
 13:  	{
 14:  		T result = operation();
 15:  		if (condition == null || condition(result))
 16:  			results.Add(result);
 17:  	}
 18:  
 19:  	return results;
 20:  }

Przytoczona lista nie jest imponująco długa, nie o to jednak chodzi. Chodzi o ideę, o eksperymentowanie, o estetykę... Niechaj nasz kod nie powoduje chęci odwiedzin u porcelanowego bożka!
Mam nadzieję, że w komentarzach zobaczymy więcej przykładów. Mam również nadzieję, że moja krucjata o nauczenie się C# 3.0 przez tych, którzy się go boją, przynosi efekty!

Opublikowane 12 listopada 08 06:35 przez Procent
Filed under: ,

Komentarze:

# Wojciech Gebczyk said on listopada 12, 2008 23:01:

Tak patrze i zaraz kojarzy mi sie pasqdne "On Error Resume Next". Brrr...

;-)

# Hellix said on listopada 13, 2008 10:56:

Pro przykłady. :) Przyzwyczajenia do lambd są w dalszej perspektywie czymś co naprawdę może wyjść każdemu na dobre. ;)

# dotnetomaniak.pl said on kwietnia 18, 2009 10:15:

Dziękujemy za publikację - Trackback z dotnetomaniak.pl

# WooBoo said on kwietnia 19, 2009 01:19:

Maćku,

W Twoich pierwszych dwóch przykładach nie widzę zastosowania lambda expressions. Używasz prostych delegatów (choć lambda to po prostu anonimowe delegaty w innym zapisie). Używając trochę generycznych typów i extension methods przykład mógłby wyglądać ciekawiej i być bardziej przydatny (np. przyjmować parametry i może coś zwracać):

   public static class MethodHelpers

   {

       public static void IgnoreExceptions<T>(this T subject, params Action<T>[] actions)

       {

           foreach (var action in actions)

           {

               try

               {

                   action.Invoke(subject);

               }

               catch(Exception ex)

               {

                   //log, show message box, do whatever you like

               }

           }

       }

       public static R IgnoreExceptions<T, R>(this T subject, Func<T, R> action)

       {

           try

           {

               return action.Invoke(subject);

           }

           catch

           {

               //log, show message box, do whatever you like

               return default(R);

           }

       }

   }

a użycie:

           SomeClass sc = new SomeClass();

           string param = "WooBoo";

           sc.IgnoreExceptions(o => o.DoSomething(param+"1")

               ,o => o.DoSomething(param+"2")

               ,o => o.DoSomething(param+"3")

               ,o => o.DoSomething(param+"4")

               ,o => o.DoSomething(param+"5"));

           string result = sc.IgnoreExceptions(o => o.SaySomething(param));

Zastanawiam się czy bawiąc się klasą Expression<T> nie dało by się wyodrębnić i opakować w try/cach wszystkich wywołań z czegoś takiego:

sc.IgnoreException(o=>{

o.DoSomething(param+"1");

o.DoSomething(param+"2");

o.DoSomething(param+"3");

});

Komentarze anonimowe wyłączone

About Procent