Wyrażenia lambda są łatwe w użyciu, ale jak to bywa z takimi ułatwieniami również nieświadomie można spowodować poważne problemy. Przykład:
class SampleClass { } class Factory { private Type _type = typeof (SampleClass); public Func<SampleClass> Create() { return () => (SampleClass)Activator.CreateInstance(_type); } } internal class Program { private static void Main(string[] args) { Task task=Task.Factory.StartNew(Run); task.Wait(); } private static Func<SampleClass> Create() { Factory factory=new Factory(); return factory.Create(); } private static void Run() { Func<SampleClass> factory = Create(); while (true) { // some logic } } }
Powyższy kod spowoduje memory leak. Dlaczego? Wyrażenie lambda potrzebuje dostęp do pola klasy Factory co skutkuje trzymaniem w pamięci całego obiektu Factory. Może być to trochę mylące bo w końcu w metodzie Program:Create, referencja Factory znajduje się po za scope i wydawałoby się, że GC usunie obiekt z pamięci.
Aby w pełni to zrozumieć warto przeanalizować z Reflector zasadę działania lambda oraz metod anonimowych. Pierwsza możliwość to przypadek gdy lambda potrzebuje dostęp do pola klasy:
class Factory { private Type _type = typeof (SampleClass); public Func<SampleClass> Create() { return () => (SampleClass)Activator.CreateInstance(_type); } }
CLR musi jakoś przedłużyć czas życia obiektowi _type. Normalnie _type zostałby zwolniony gdy nie ma referencji do Factory. Zaglądając do wygenerowanego kodu, można dowiedzieć się, że została stworzona nowa metoda w klasie Factory. Podstawowa implementacja Create wygląda następująco:
.method public hidebysig instance class [mscorlib]System.Func`1<class SampleClass> Create() cil managed { .maxstack 3 .locals init ( [0] class [mscorlib]System.Func`1<class SampleClass> CS$1$0000) L_0000: nop L_0001: ldarg.0 L_0002: ldftn instance class SampleClass Factory::<Create>b__0() L_0008: newobj instance void [mscorlib]System.Func`1<class SampleClass>::.ctor(object, native int) L_000d: stloc.0 L_000e: br.s L_0010 L_0010: ldloc.0 L_0011: ret }
Jak widać, zamiast lambdy, zwracana jest nowo wygenerowana metoda o nazwie <Create>b__0():
.method private hidebysig instance class SampleClass <Create>b__0() cil managed { .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() .maxstack 1 .locals init ( [0] class SampleClass CS$1$0000) L_0000: ldarg.0 L_0001: ldfld class [mscorlib]System.Type Factory::_type L_0006: call object [mscorlib]System.Activator::CreateInstance(class [mscorlib]System.Type) L_000b: castclass SampleClass L_0010: stloc.0 L_0011: br.s L_0013 L_0013: ldloc.0 L_0014: ret }
Powyższe przykłady wyjaśniają memory leak przedstawiony na początku artykułu. W praktyce, nowa metoda jest tworzona a następnie zwracany jest do niej wskaźnik. Dzieje się tak ponieważ lambda potrzebuje dostęp do prywatnego pola. Nowa utworzona metoda oczywiście taki dostęp będzie miała. Bardziej skomplikowanym przypadkiem jest dostęp do pól lokalnych np.:
class Factory { public Func<SampleClass> Create() { SampleClass sampleClass=new SampleClass(); return () => sampleClass; } }
Wygenerowanie nowej metody nie rozwiąże problemu – metody nie mają dostępu do zmiennych lokalnych innych metod. CLR wygeneruje nową klasę opakowująca, która jest zagnieżdżona w Factory:
class private auto ansi beforefieldinit Factory extends [mscorlib]System.Object { .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { } .method public hidebysig instance class [mscorlib]System.Func`1<class SampleClass> Create() cil managed { } .class auto ansi sealed nested private beforefieldinit <>c__DisplayClass1 extends [mscorlib]System.Object { .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { } .method public hidebysig instance class SampleClass <Create>b__0() cil managed { } .field public class SampleClass sampleClass } }
Nowa klasa DisplayClass1 posiada pole SampleClass oraz metodę Createb_0. Innymi słowy, lambda została przeniesiona do nowej klasy (DisplayClass1). Logika zawarta jest w Createb__0 a dane (SampleClass) w publicznym polu. Następnie w oryginalnej metodzie Factory:Create zamiast wywołania zwykłej metody należy stworzyć obiekt DisplayClass1 i przypisać SampleClass do pola publicznego:
.method public hidebysig instance class [mscorlib]System.Func`1<class SampleClass> Create() cil managed { .maxstack 3 .locals init ( [0] class Factory/<>c__DisplayClass1 CS$<>8__locals2, [1] class [mscorlib]System.Func`1<class SampleClass> CS$1$0000) L_0000: newobj instance void Factory/<>c__DisplayClass1::.ctor() L_0005: stloc.0 L_0006: nop L_0007: ldloc.0 L_0008: newobj instance void SampleClass::.ctor() L_000d: stfld class SampleClass Factory/<>c__DisplayClass1::sampleClass L_0012: ldloc.0 L_0013: ldftn instance class SampleClass Factory/<>c__DisplayClass1::<Create>b__0() L_0019: newobj instance void [mscorlib]System.Func`1<class SampleClass>::.ctor(object, native int) L_001e: stloc.1 L_001f: br.s L_0021 L_0021: ldloc.1 L_0022: ret }
Linia “L_000d: stfld class SampleClass Factory/<>c__DisplayClass1::sampleClass” przypisuje utworzoną instancję SampleClass do pola publicznego obiektu DisplayClass1. Reszta myślę, że powinna być jasna.
Co w przypadku dostępu do obiektów statycznych? Sprawdźmy:
class Factory { static SampleClass _sampleClass=new SampleClass(); public Func<SampleClass> Create() { return () => _sampleClass; } }
Zostanie wygenerowana po prostu nowa metoda ale statyczna:
.class private auto ansi beforefieldinit Factory extends [mscorlib]System.Object { .method private hidebysig specialname rtspecialname static void .cctor() cil managed { } .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { } .method private hidebysig static class SampleClass <Create>b__0() cil managed { .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() } .method public hidebysig instance class [mscorlib]System.Func`1<class SampleClass> Create() cil managed { } .field private static class SampleClass _sampleClass .field private static class [mscorlib]System.Func`1<class SampleClass> CS$<>9__CachedAnonymousMethodDelegate1 { .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() } }
Kolejna interesująca kwestia to przekazanie dwóch lokalnych zmiennych o różnym scope np.:
class Factory { public Func<SampleClass> Create() { int i = 0; // scope I if(i>=0) { int j = 0; // scope II return () => new SampleClass(i,j); } return null; } }
CLR wygeneruje dwie klasy. Jedna będzie przechowywać zmienną “i” (scope I) druga z kolei będzie przechowywać “j”(scope II), metodę oraz referencję do wrapper’a pierwszego ( w formie pola publicznego):
.class private auto ansi beforefieldinit Factory extends [mscorlib]System.Object { .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { } .method public hidebysig instance class [mscorlib]System.Func`1<class SampleClass> Create() cil managed { } .class auto ansi sealed nested private beforefieldinit <>c__DisplayClass1 extends [mscorlib]System.Object { .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { } .field public int32 i } .class auto ansi sealed nested private beforefieldinit <>c__DisplayClass3 extends [mscorlib]System.Object { .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { } .method public hidebysig instance class SampleClass <Create>b__0() cil managed { } .field public class Factory/<>c__DisplayClass1 CS$<>8__locals2 .field public int32 j } }
Oraz przykład użycia wrapper’ow:
.method public hidebysig instance class [mscorlib]System.Func`1<class SampleClass> Create() cil managed { .maxstack 3 .locals init ( [0] class Factory/<>c__DisplayClass3 CS$<>8__locals4, [1] class Factory/<>c__DisplayClass1 CS$<>8__locals2, [2] class [mscorlib]System.Func`1<class SampleClass> CS$1$0000, [3] bool CS$4$0001) L_0000: newobj instance void Factory/<>c__DisplayClass1::.ctor() L_0005: stloc.1 L_0006: nop L_0007: ldloc.1 L_0008: ldc.i4.0 L_0009: stfld int32 Factory/<>c__DisplayClass1::i L_000e: ldloc.1 L_000f: ldfld int32 Factory/<>c__DisplayClass1::i L_0014: ldc.i4.0 L_0015: clt L_0017: stloc.3 L_0018: ldloc.3 L_0019: brtrue.s L_003f L_001b: newobj instance void Factory/<>c__DisplayClass3::.ctor() L_0020: stloc.0 L_0021: ldloc.0 L_0022: ldloc.1 L_0023: stfld class Factory/<>c__DisplayClass1 Factory/<>c__DisplayClass3::CS$<>8__locals2 L_0028: nop L_0029: ldloc.0 L_002a: ldc.i4.0 L_002b: stfld int32 Factory/<>c__DisplayClass3::j L_0030: ldloc.0 L_0031: ldftn instance class SampleClass Factory/<>c__DisplayClass3::<Create>b__0() L_0037: newobj instance void [mscorlib]System.Func`1<class SampleClass>::.ctor(object, native int) L_003c: stloc.2 L_003d: br.s L_0043 L_003f: ldnull L_0040: stloc.2 L_0041: br.s L_0043 L_0043: ldloc.2 L_0044: ret }
Gdyby wszystkie zmienne miały ten sam poziom scope wtedy zostałby wygenerowany tylko jeden wrapper:
internal class Factory { public Func<SampleClass> Create() { int i = 0; // scope I int j = 0; // scope I return () => new SampleClass(i, j); } }
.class private auto ansi beforefieldinit Factory extends [mscorlib]System.Object { .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { } .method public hidebysig instance class [mscorlib]System.Func`1<class SampleClass> Create() cil managed { } .class auto ansi sealed nested private beforefieldinit <>c__DisplayClass1 extends [mscorlib]System.Object { .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { } .method public hidebysig instance class SampleClass <Create>b__0() cil managed { } .field public int32 i .field public int32 j } }
Ostatni przypadek jaki chciałbym omówić jest dostęp do pól zarówno lokalnych jak i klasy:
internal class Factory { private readonly SampleClass _sampleClass = new SampleClass(); public Func<SampleClass> Create() { int i = 0; // scope I return delegate { _sampleClass.SetValue(i);// field return _sampleClass; }; } }
Zostanie wygenerowany wrapper – to oczywiste ponieważ wymagany jest dostęp do danych lokalnych. Następnie wrapper ma dostęp do klasy Factory, która z kolei posiada dostęp do pola _sampleClass:
.class private auto ansi beforefieldinit Factory extends [mscorlib]System.Object { .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { } .method public hidebysig instance class [mscorlib]System.Func`1<class SampleClass> Create() cil managed { } .field private initonly class SampleClass _sampleClass .class auto ansi sealed nested private beforefieldinit <>c__DisplayClass1 extends [mscorlib]System.Object { .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { } .method public hidebysig instance class SampleClass <Create>b__0() cil managed { } .field public class Factory <>4__this .field public int32 i }
Przed zwróceniem delegat’y należy zatem stworzyć instancję DisplayClass1 i ustawić pole “i”(zmienna lokalna) oraz Factory (dostęp do _sampleClass):
.method public hidebysig instance class [mscorlib]System.Func`1<class SampleClass> Create() cil managed { .maxstack 3 .locals init ( [0] class Factory/<>c__DisplayClass1 CS$<>8__locals2, [1] class [mscorlib]System.Func`1<class SampleClass> CS$1$0000) L_0000: newobj instance void Factory/<>c__DisplayClass1::.ctor() L_0005: stloc.0 L_0006: ldloc.0 L_0007: ldarg.0 L_0008: stfld class Factory Factory/<>c__DisplayClass1::<>4__this L_000d: nop L_000e: ldloc.0 L_000f: ldc.i4.0 L_0010: stfld int32 Factory/<>c__DisplayClass1::i L_0015: ldloc.0 L_0016: ldftn instance class SampleClass Factory/<>c__DisplayClass1::<Create>b__0() L_001c: newobj instance void [mscorlib]System.Func`1<class SampleClass>::.ctor(object, native int) L_0021: stloc.1 L_0022: br.s L_0024 L_0024: ldloc.1 L_0025: ret }
Kiedyś pisałem już o niespodziankach związanych z lambda. Polecam post “Wyrażenia lambda i niespodziewany rezultat” , przestawiający generowanie DisplacClass dla pętli.