Kiedyś czytając książkę “More Effective C#” zaciekawiło mnie wyjaśnienie interpretacji wyrażeń lambda przez kompilator. W książce autor przedstawił następujący fragment kodu:
public class ModFilter { private readonly int modulus; public ModFilter(int mod) { modulus = mod; } public IEnumerable<int> FindValues( IEnumerable<int> sequence) { return from n in sequence where n % modulus == 0 select n * n; } }
W rzeczywistości wyrażenie zostanie skonwertowane do znanych już od dawna delegat:
public class ModFilter { private readonly int modulus; private bool WhereClause(int n) { return ((n % this.modulus) == 0); } private static int SelectClause(int n) { return (n * n); } private static Func<int, int> SelectDelegate; public IEnumerable<int> FindValues( IEnumerable<int> sequence) { if (SelectDelegate == null) { SelectDelegate = new Func<int, int>(SelectClause); } return sequence.Where<int>( new Func<int, bool>(this.WhereClause)). Select<int, int>(SelectClause); } }
Kompilator wygenerował metody WhereClause oraz SelectClause. Ciekawszą jednak obserwacją jest przypadek w którym wykorzystujemy zmienne lokalne:
public class ModFilter { private readonly int modulus; public ModFilter(int mod) { modulus = mod; } public IEnumerable<int> FindValues( IEnumerable<int> sequence) { int numValues = 0; return from n in sequence where n % modulus == 0 select n * n / ++numValues; } }
Zmienna numValues jest lokalna. W jaki sposób zostanie to zinterpretowane? Otóż kompilator wygeneruje klasę zagnieżdżoną:
public class ModFilter { private sealed class Closure { public ModFilter outer; public int numValues; public int SelectClause(int n) { return ((n * n) / ++this.numValues); } } private readonly int modulus; public ModFilter(int mod) { this.modulus = mod; } private bool WhereClause(int n) { return ((n % this.modulus) == 0); } public IEnumerable<int> FindValues (IEnumerable<int> sequence) { Closure c = new Closure(); c.outer = this; c.numValues = 0; return sequence.Where<int> (new Func<int, bool>(this.WhereClause)) .Select<int, int>( new Func<int, int>(c.SelectClause)); } }
Closure posiada dostęp do zewnętrznej klasy ModFilter. Wszystkie zmienne lokalne (w tym przypadku numValues) zostały po prostu przekopiowane. Używając wyrażeń lambda łatwo zapomnieć o tym, że tak naprawdę tworzone są osobne metody i wszystkie parametry lokalne muszą zostać przekopiowane. Wyrażenia znacznie ułatwiają i przyśpieszają pisanie kodu ale z drugiej strony równie łatwo można popełnić błąd. Autor książki (Bill Wagner) przytacza następujący kod jako przestrogę:
int index = 0; Func<IEnumerable<int>> sequence = () => Utilities.Generate(30, () => index++); index = 20; foreach (int n in sequence()) Console.WriteLine(n); Console.WriteLine("Done"); index = 100; foreach (int n in sequence()) Console.WriteLine(n);
Kod wydrukuje wartości od 20-50 a następnie od 100-130. Według autora może to być zaskakujące ponieważ funkcja definiowana jest dla index=0. Jednak ze względu na to sposób interpretacji (closure) wartość zmiennej jest przekopiowywana i generowanie nie zaczyna się od 0 a od 20 oraz 100. Moim zdaniem jest to akurat bardzo intuicyjne, aczkolwiek należy zdawać sobie sprawę ze sposobu realizacji lambda w C#.