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