W jaki sposób interpretowane są wyrażenia lambda?

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

Leave a Reply

Your email address will not be published.