2009-05-25

Zmienne lokalne, a wyrażenia lamba i anonimowe delegaty.

Przykład zastosowania anonimowego delegata:
KeyUp += delegate(object sender, KeyEventArgs e) {};
Przykład zastosowania wyrażenia lambda:
KeyUp += (sender, e) => { };
Dla zrozumienia o czym tutaj będzie mowa, spójrzmy na to, jak to wygląda po skompilowaniu do kodu pośredniego. Wykorzystamy tutaj program Reflector. Przykładowy kod:
delegate void TestDelegate();

public void Test3()
{
    KeyUp += delegate(object sender, KeyEventArgs e) { };
    KeyUp += (sender, e) => { };

    TestDelegate td = Test3;
}
Skompilowany kod przy użyciu .NET 3.5:
private delegate void TestDelegate();

public void Test3()
{
    base.KeyUp += delegate (object sender, KeyEventArgs e) {
    };
    base.KeyUp += delegate (object sender, KeyEventArgs e) {
    };
    TestDelegate td = new TestDelegate(this.Test3);
}
Widzimy, że wyrażenia lambda to nic innego jak anonimowy delegat zapisany inaczej. A tak wygląda kod przy użyciu .NET 1.0:
private delegate void TestDelegate();

private static void <Test3>b__0(object sender, KeyEventArgs e)
{
}

private static void <Test3>b__1(object sender, KeyEventArgs e)
{
}

public void Test3()
{
    base.KeyUp += ((CS$<>9__CachedAnonymousMethodDelegate2 != null) ? CS$<>9__CachedAnonymousMethodDelegate2 : (CS$<>9__CachedAnonymousMethodDelegate2 = new KeyEventHandler(Form1.<Test3>b__0)));
    base.KeyUp += ((CS$<>9__CachedAnonymousMethodDelegate3 != null) ? CS$<>9__CachedAnonymousMethodDelegate3 : (CS$<>9__CachedAnonymousMethodDelegate3 = new KeyEventHandler(Form1.<Test3>b__1)));
    TestDelegate td = new TestDelegate(this.Test3);
}

private static KeyEventHandler CS$<>9__CachedAnonymousMethodDelegate2;

private static KeyEventHandler CS$<>9__CachedAnonymousMethodDelegate3;
Anonimowe delegaty zostały wprowadzone wraz z .NET 2.0. Jednakże na poziomie kodu pośredniego nie wprowadzono żadnych nowych elementów. Anonimowe delegaty i wyrażenia lamba to delegaty do automatycznie generowanych przez kompilator metod. Z tym wiąże się parę ograniczeń. We wnętrzu anonimowej metody nie możemy użyć goto, continue, break, tak, że przekazanie sterowania nastąpi poza blok anonimowy.
while (true)
{
    KeyUp += delegate(object sender, KeyEventArgs e)
    {
        break;
    };
}
Anonimowa metoda nie może się odwoływać do parametrów ref, out metody zewnętrznej. Metoda anonimowa to automatycznie generowana przez kompilator metoda.
public void Test4(ref int dd, out int ee)
{
    KeyUp += delegate(object sender, KeyEventArgs e)
    {
        dd++;
        ee = 5;
    };
}

public void Test5(ref Int32 dd, out Int32 ee)
{
    while (true)
    {
        KeyUp += (sender, e) => 
        {
            dd = 56;
            ee = 67;
        };
    }
}
W przypadku parametru out ciężko jest definiować co ma zwracać funkcja, kiedy ona już dawno mogła się zakończyć. W przypadku parametru ref w momencie wywołania anonimowej metody w pierwszej metodzie zmienna może być zdefiniowana na stosie i zwyczajnie już nie istnieć, w drugiej metodzie może nie istnieć referencja do zmiennej (Int32 ddd = 5;). Oto typowy przypadek gdy metoda anonimowa korzysta ze zmiennych spoza swego zakresu:
public partial class Form1 : Form
{
    public int eee;

    public class TestValue
    {
        public int v;
    }

    public Form1()
    {
        InitializeComponent();
        TestValue v = new TestValue() { v = 56 };
        Test6(5, v);
    }

    public void Test6(int dd, TestValue v)
    {
        KeyUp += delegate(object sender, KeyEventArgs e)
        {
            v.v = 67;
            dd++;
            eee++;
        };
    }
}
Tak wygląd ten kod w Reflectorze:
private sealed class <>c__DisplayClass2
{
    // Fields
    public Form1 <>4__this;
    public int dd;
    public Form1.TestValue v;

    // Methods
    public void <Test6>b__1(object sender, KeyEventArgs e)
    {
        this.v.v = 0x43;
        this.dd++;
        this.<>4__this.eee++;
    }
}

.method public hidebysig instance void Test6(int32 dd, class TestApp.Form1/TestValue v) cil managed
{
    .maxstack 4
    .locals init (
        [0] class TestApp.Form1/<>c__DisplayClass2 CS$<>8__locals3)
    L_0000: newobj instance void TestApp.Form1/<>c__DisplayClass2::.ctor()
    L_0005: stloc.0 
    L_0006: ldloc.0 
    L_0007: ldarg.1 
    L_0008: stfld int32 TestApp.Form1/<>c__DisplayClass2::dd
    L_000d: ldloc.0 
    L_000e: ldarg.2 
    L_000f: stfld class TestApp.Form1/TestValue TestApp.Form1/<>c__DisplayClass2::v
    L_0014: ldloc.0 
    L_0015: ldarg.0 
    L_0016: stfld class TestApp.Form1 TestApp.Form1/<>c__DisplayClass2::<>4__this
    L_001b: nop 
    L_001c: ldarg.0 
    L_001d: ldloc.0 
    L_001e: ldftn instance void TestApp.Form1/<>c__DisplayClass2::<Test6>b__1(object, class [System.Windows.Forms]System.Windows.Forms.KeyEventArgs)
    L_0024: newobj instance void [System.Windows.Forms]System.Windows.Forms.KeyEventHandler::.ctor(object, native int)
    L_0029: call instance void [System.Windows.Forms]System.Windows.Forms.Control::add_KeyUp(class [System.Windows.Forms]System.Windows.Forms.KeyEventHandler)
    L_002e: nop 
    L_002f: nop 
    L_0030: ret 
}
Metoda Test6 zapisana jest w IL, gdyż tylko wtedy widzimy co się dzieje. Deklarowana jest zmienna lokalna do generowanej automatycznie klasy c__DisplayClass1. Następnie tworzony jest obiekt tej klasy. Jego pola są następnie odpowiednio inicjalizowane. Dla parametru typu wartościowego dd tworzone jest pole wartościowe, a wartość zmiennej jest do niego kopiowana. Dla parametru typu referencyjnego v zapamiętywana jest referencja do obiektu. Dodatkowo we wnętrzu metody anonimowej inkrementujemy pole eee obiektu zewnętrznego. Kompilator zapamiętuje tutaj referencje do tego obiektu. Tutaj możemy powiedzieć o dwóch rzeczach. Po pierwsze dopóki zdarzenie z tak napisaną metodą anonimową nie zostanie odrejestrowane, będzie ono utrzymywać referencje do w tym wypadku dwóch obiektów. Może ale nie musi w niektórych wypadkach prowadzić do jakiś trudnych w zdebagowaniu błędów. No i po drugie jest wyraźna różnica pomiędzy modyfikowanie zmiennej referencyjnej, a wartościowej w metodzie anonimowej (operowanie na referencji zmiennej, a operaowanie na kopii zmiennej). Poza tym zauważmy różnicę pomiędzy kodem generowanym w tym przypadku (obiekt klasy), a poprzednim (statyczne zmienne i metody). Tutaj dla każdego tworzenia delegacji do metody anonimowej musi zostać zapamiętany inny za każdym razem stan zmiennych używanych w metodach anonimowych. Pozostało nam jeszcze potestować bardziej skomplikowane sytuacje:
public partial class Form1 : Form
{
    public class TestValue
    {
        public int v;
    }

    public Form1()
    {
        InitializeComponent();
        Test7();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        Close();
    }

    private Func<int> GetDelegate(int v, TestValue r)
    {
        r.v++;
        v++;

        System.Console.WriteLine("r: {0}", r.v); // 6
        System.Console.WriteLine("v: {0}", v); // 6

        Func<int> del = () =>
        {
            r.v++;
            v++;

            System.Console.WriteLine("r: {0}", r.v); // 8
            System.Console.WriteLine("v: {0}", v); // 8

            return 0;
        };

        r.v++;
        v++;

        System.Console.WriteLine("r: {0}", r.v); // 7
        System.Console.WriteLine("v: {0}", v); // 7

        return del;
    }

    private void Test7()
    {
        int v = 5;
        TestValue r = new TestValue() { v = 5 };

        System.Console.WriteLine("r: {0}", r.v); // 5
        System.Console.WriteLine("v: {0}", v); // 5

        Func<int> del = GetDelegate(v, r);

        System.Console.WriteLine("r: {0}", r.v); // 7
        System.Console.WriteLine("v: {0}", v); // 5

        del();

        System.Console.WriteLine("r: {0}", r.v); // 8
        System.Console.WriteLine("v: {0}", v); // 5

        r.v++;
        v++;

        System.Console.WriteLine("r: {0}", r.v); // 9
        System.Console.WriteLine("v: {0}", v); // 6
    }
}
W komentarzach są podane jakie wartości przybierają zmienne r i v. Jedyne co się nie zgadza to wartości podawana przez metodę anonimową. Przed jej utworzeniem zmienna v ma wartość 6. Jest ona kopiowana do ukrytego obiektu generowanego przez kompilator. Po wywołaniu metody anonimowej jej wartość powinna wynosić 7 a nie 8. Co się dzieje, oto kod IL (metody wypisujące wartości na konsole zostały zakomentowane):
.method private hidebysig instance class [System.Core]System.Func`1<int32> GetDelegate(int32 v, class TestApp.Form1/TestValue r) cil managed
{
    .maxstack 3
    .locals init (
        [0] class [System.Core]System.Func`1<int32> del,
        [1] class TestApp.Form1/<>c__DisplayClass1 CS$<>8__locals2,
        [2] class [System.Core]System.Func`1<int32> CS$1$0000)
    L_0000: newobj instance void TestApp.Form1/<>c__DisplayClass1::.ctor()
    L_0005: stloc.1 
    L_0006: ldloc.1 
    L_0007: ldarg.1 
    L_0008: stfld int32 TestApp.Form1/<>c__DisplayClass1::v
    L_000d: ldloc.1 
    L_000e: ldarg.2 
    L_000f: stfld class TestApp.Form1/TestValue TestApp.Form1/<>c__DisplayClass1::r
    L_0014: nop 
    L_0015: ldloc.1 
    L_0016: ldfld class TestApp.Form1/TestValue TestApp.Form1/<>c__DisplayClass1::r
    L_001b: dup 
    L_001c: ldfld int32 TestApp.Form1/TestValue::v
    L_0021: ldc.i4.1 
    L_0022: add 
    L_0023: stfld int32 TestApp.Form1/TestValue::v
    L_0028: ldloc.1 
    L_0029: dup 
    L_002a: ldfld int32 TestApp.Form1/<>c__DisplayClass1::v
    L_002f: ldc.i4.1 
    L_0030: add 
    L_0031: stfld int32 TestApp.Form1/<>c__DisplayClass1::v
    L_0036: ldloc.1 
    L_0037: ldftn instance int32 TestApp.Form1/<>c__DisplayClass1::<GetDelegate>b__0()
    L_003d: newobj instance void [System.Core]System.Func`1<int32>::.ctor(object, native int)
    L_0042: stloc.0 
    L_0043: ldloc.1 
    L_0044: ldfld class TestApp.Form1/TestValue TestApp.Form1/<>c__DisplayClass1::r
    L_0049: dup 
    L_004a: ldfld int32 TestApp.Form1/TestValue::v
    L_004f: ldc.i4.1 
    L_0050: add 
    L_0051: stfld int32 TestApp.Form1/TestValue::v
    L_0056: ldloc.1 
    L_0057: dup 
    L_0058: ldfld int32 TestApp.Form1/<>c__DisplayClass1::v
    L_005d: ldc.i4.1 
    L_005e: add 
    L_005f: stfld int32 TestApp.Form1/<>c__DisplayClass1::v
    L_0064: ldloc.0 
    L_0065: stloc.2 
    L_0066: br.s L_0068
    L_0068: ldloc.2 
    L_0069: ret 
}
Najważniejsza jest linia 7. Zmienna v jest ładowana i zapamiętywana w delegacie, dalej wszelkie operacje są wykonywane na zmiennej delegacji, nawet inkrementacje po stworzeniu delegacji. To tutaj właśnie powstaje to +1 dla wartości v. Ciężko takie zachowanie nazwać poprawnym.

Brak komentarzy:

Prześlij komentarz