2009-05-29

Słowa kluczowe ref i out

Słowa kluczowe ref i out wykorzystywane są jako dodatkowe parametry parametrów metod. Przykład zastosowania:
public class TestClass
{
    public int X = 1;
}

public void Test1(ref int x, ref TestClass tc1, ref TestClass tc2)
{
    x++;
    tc1 = new TestClass() { X = 5 };
    tc2.X = 6;
}

public void Test2(out int x, out TestClass tc)
{
    x = 8;
    tc = new TestClass() { X = 6 };
}

public void Test()
{
    int x1 = 5;

    TestClass tc1 = new TestClass() { X = 5 };
    TestClass tc2 = new TestClass() { X = 5 };

    TestClass tc1c = tc1;
    TestClass tc2c = tc2;

    System.Console.WriteLine(x1); // 5
    System.Console.WriteLine(tc1.X); // 5
    System.Console.WriteLine(tc2.X); // 5

    Test1(ref x1, ref tc1, ref tc2);

    System.Console.WriteLine(x1); // 6
    System.Console.WriteLine(tc1.X); // 5
    System.Console.WriteLine(tc2.X); // 6

    System.Console.WriteLine(Object.ReferenceEquals(tc1, tc1c)); // False
    System.Console.WriteLine(Object.ReferenceEquals(tc2, tc2c)); // True

    int x2;
    TestClass tc3;

    Test2(out x2, out tc3);

    System.Console.WriteLine(x2); // 8
    System.Console.WriteLine(tc3.X); // 6
}
Czyli słowo kluczowe ref jak sama nazwa wskazuje, służy do przekazywania metodzie referencji do zmiennej. Każda zmiana zmiennej w metodzie oznaczonej jako ref powoduje zmiany w zmiennej referencyjnej, jeśli możemy ją tak nazwać. Warto tutaj zwrócić uwagę na różnicę pomiędzy referencją do zmiennej, a referencją do obiektu. Przed przekazaniem zmiennej do metody jako referencji zmienna ta musi być zainicjalizowana. Nie dotyczy to out, który jest szczególnym przypadkiem ref, tutaj możemy przekazać do metody zmienną jako referencje bez jej inicjalizacji. W drugą stronę parametr metody oznaczony przez ref może być przez tą metodę nietknięty, zaś w przypadku out inicjalizacja takiego parametru musi nastąpić (tutaj mamy duże podobieństwo do return). Czy moglibyśmy się bez tych słów kluczowych obejść ? Tak, ale wiele rzeczy byłoby wtedy bardziej skomplikowanych. Słowo kluczowe ref pozwala na szybkie przekazywanie do metod typów wartościowych (w tym struktur) bez ich kopiowania. Pozwala metodzie na modyfikowania wielu zmiennych wartościowych przekazywanych przez referencję. Normalnie można modyfikować tylko jedną: int r = 5; r = Test5(r);. No i pozwala na modyfikowanie referencji do obiektów. Słowo kluczowe out pozwala na zwracanie przez metodę wielowartościowego rezultatu. I jako efekt uboczny typy wartościowe nie podlegają kopiowaniu. Zauważmy, że wywołując metodę której parametry są oznaczone jako ref i out musimy te parametry oznaczyć także tymi samymi słowami kluczowymi. Dzięki temu wiemy jakiego zachowania po metodzie możemy się spodziewać. Dla obu słów kluczowych do metod nie możemy przekazywać właściwości obiektów. Jak to wygląda od strony IL:
public void Test1(ref int x1, ref TestClass tc1, out int x2, out TestClass tc2)
{
    x2 = 5;
    tc2 = new TestClass();
}

public void Test()
{
    int x1 = 5;
    TestClass tc1 = new TestClass();
    int x2;
    TestClass tc2;

    Test1(ref x1, ref tc1, out x2, out tc2);
}
Tutaj jedno uzupełnienie. Takie wywołanie jest niemożliwe Test1(ref 5, ref new TestClass(), out 5, out new TestClass());. Poprzez ref i out możemy tylko przekazać konkretne zmienne. Wracając do wyglądu kodu w IL:
.method public hidebysig instance void Test1(int32& x1, class TestApp.Form1/TestClass& tc1, [out] int32& x2, [out] class TestApp.Form1/TestClass& tc2) cil managed
{
    .maxstack 8
    L_0000: nop 
    L_0001: ldarg.3 
    L_0002: ldc.i4.5 
    L_0003: stind.i4 
    L_0004: ldarg.s tc2
    L_0006: newobj instance void TestApp.Form1/TestClass::.ctor()
    L_000b: stind.ref 
    L_000c: ret 
}

.method public hidebysig instance void Test() cil managed
{
    .maxstack 5
    .locals init (
        [0] int32 x1,
        [1] class TestApp.Form1/TestClass tc1,
        [2] int32 x2,
        [3] class TestApp.Form1/TestClass tc2)
    L_0000: nop 
    L_0001: ldc.i4.5 
    L_0002: stloc.0 
    L_0003: newobj instance void TestApp.Form1/TestClass::.ctor()
    L_0008: stloc.1 
    L_0009: ldarg.0 
    L_000a: ldloca.s x1
    L_000c: ldloca.s tc1
    L_000e: ldloca.s x2
    L_0010: ldloca.s tc2
    L_0012: call instance void TestApp.Form1::Test1(int32&, class TestApp.Form1/TestClass&, int32&, class TestApp.Form1/TestClass&)
    L_0017: nop 
    L_0018: ret 
}
Jak widzimy informacją czy dany parametr jest ref, czy out jest tam widoczna. Widać też tam wyraźnie, że out jest szczególnym przypadkiem ref. Pozostała nam jeszcze ostatnia rzecz do ustalenia co się dzieje przy przeciążaniu metod. Ponieważ przy wywołaniu metod trzeba jawnie podać ref, czy out, takie metody są w pełni rozróżnialne:
public void Test1(ref int x1)
{
}

public void Test1(int x1)
{
}

public void Test()
{
    int x = 6;
    Test1(x);
    Test1(x);
}
Ale tego typu metody uznawane są jako podwójne, pomimo tego, że jak widać w IL, można je bez problemu rozróżnić:
public void Test1(ref int x1)
{
}

public void Test1(out int x1)
{
    x1 = 4;
}

public void Test()
{
    int x = 6;
    Test1(x);
    Test1(x);
}

2009-05-27

Metody rozszerzające.

Jest to funkcjonalność języka dodana w wersji .NET 3.5. Nie wymaga ona żadnych modyfikacji po stronie IL, jest to tak zwany syntactic sugar. Na metodach rozszerzających zbudowane jest całe LINQ. Metody rozszerzające umożliwiają uzupełnienie już istniejących klas, a także ich wszystkich podklas o nowe metody. Dotyczy to także interfejsów, typów wartościowych, tablic. Jeśli istnieje metoda składowa klasy, którą to metoda rozszerzająca nadpisuje, to metoda rozszerzająca nigdy nie zostanie wywołana. Metody rozszerzające muszą być deklarowane w klasach statycznych. Sama musi być statyczna. Od innych metod różni się użyciem słowa kluczowego this. Aby móc użyć danej metody rozszerzające musimy za pomocą using dostarczyć przestrzeń nazw w której zawarta jest klasa statyczna z taką metodą. Metoda rozszerzająca staje się metodą obiektu, nie można w ten sposób poszerzać funkcjonalności klas. Przykład metody rozszerzającej:
public static int DoubleVal(this int i)
{
   return i * 2;
}

public static void Test1()
{
   int v = 5;
   v = v.DoubleVal();
   System.Console.WriteLine(v);
}
Pierwszym parametrem metody rozszerzającej jest obiekt na którym metoda działa. Nic nie szkodzi na przeszkodzie aby metoda rozszerzająca miała parametry:
public static int Modulo(this int i, int m)
{
   return i % m;
}

public static void Test1()
{
   int v = 5;
   v = v.Modulo(2);
   System.Console.WriteLine(v);
}
Tak to wygląda po stronie IL:
.method public hidebysig static int32 Modulo(int32 i, int32 m) cil managed
{
   .custom instance void [System.Core]System.Runtime.CompilerServices.ExtensionAttribute::.ctor()
   .maxstack 2
   .locals init (
       [0] int32 CS$1$0000)
   L_0000: nop
   L_0001: ldarg.0
   L_0002: ldarg.1
   L_0003: rem
   L_0004: stloc.0
   L_0005: br.s L_0007
   L_0007: ldloc.0
   L_0008: ret
}

.method public hidebysig static void Test1() cil managed
{
   .maxstack 2
   .locals init (
       [0] int32 v)
   L_0000: nop
   L_0001: ldc.i4.5
   L_0002: stloc.0
   L_0003: ldloc.0
   L_0004: ldc.i4.2
   L_0005: call int32 TestApp.Extensions::Modulo(int32, int32)
   L_000a: stloc.0
   L_000b: ldloc.0
   L_000c: call void [mscorlib]System.Console::WriteLine(int32)
   L_0011: nop
   L_0012: ret
}
Jak widać metoda rozszerzająca to tak naprawdę zwykła dwuparametrowa metoda. Przydatność takiego rozwiązania jest bez przecenienia. Dzięki temu możemy rozszerzyć funkcjonalność klas, które są zamknięte (sealed). Pewne funkcjonalności możemy pogrupować w kilka rozszerzeń i dołączać je tylko w razie potrzeby. Bardzo często zdarza mi się robić szereg drobnych metod, które teraz można zapisać za pomocą metod rozszerzających, co znacznie upraszcza pamiętanie, gdzie się one znajdują (jak są dostępne). Spróbujmy się teraz zagłębić w tym co kieruje kompilatorem, że wybiera tą, a nie inną metodę. Powiedziałem, że metoda obiektu jest wybierana zawsze, zamiast takiej samej istniejącej metody rozszerzające. Na początek przypominam, że dwie metody są takie same jeśli mają taką samą listę parametrów i taką samą nazwę. Parametr zwracany nie ma tutaj znaczenia. Zanim kompilator zacznie szukać metody w rozszerzeniach, najpierw stara się dopasować metodę w klasie, a dopiero później w rozszerzeniach, nawet jeśli metoda rozszerzona jest bardziej dopasowana. Przykład:
namespace TestApp
{
   public class TestClass
   {
       public void TestMethod1()
       {
           System.Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod());
       }

       public static implicit operator int(TestClass tc)
       {
           System.Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod());
           return 12;
       }

       public void TestMethod1(int x)
       {
           System.Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod());
       }

       public void TestMethod1(object x)
       {
           System.Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod());
       }   
   }

   public static class Extensions
   {
       public static void TestMethod1(this TestClass tc)
       {  
           System.Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod());
       }

       public static void TestMethod1(this TestClass tc, int x)
       {  
           System.Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod());
       }

       public static void TestMethod1(this TestClass tc, object x)
       {  
           System.Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod());
       }

       public static void Test2()
       {
           TestClass tc = new TestClass();
           object ob = 5;
           int x = 5;

           tc.TestMethod1();
           tc.TestMethod1(tc);
           tc.TestMethod1(ob);
           tc.TestMethod1(x);
       }
   }

   public partial class Form1 : Form
   {
       public Form1()
       {
           InitializeComponent();
           Extensions.Test2();
       }
   }
}
Wynik: Void TestMethod1() Int32 op_Implicit(TestApp.TestClass) Void TestMethod1(Int32) Void TestMethod1(System.Object) Void TestMethod1(Int32) Żadna metoda rozszerzona nie została wywołana. Komentując operator rzutowania sprawiamy, że tc.TestMethod1(tc) wywoła public void TestMethod1(object x). Komentując dodatkowo metodę public void TestMethod1(int x) wszystko będzie przechodzić przez public void TestMethod1(object x) dając wynik: Void TestMethod1() Void TestMethod1(System.Object) Void TestMethod1(System.Object) Void TestMethod1(System.Object) Zauważmy, że dla wywołania tc.TestMethod1(x); istnieje lepiej dopasowana metoda rozszerzona. Rozpatrzmy przypadek kiedy jest zakomentowana tylko ostatnia metoda public void TestMethod1(object x). Wtedy dostajemy wynik: Void TestMethod1() Int32 op_Implicit(TestApp.TestClass) Void TestMethod1(Int32) Void TestMethod1(TestApp.TestClass, System.Object) Void TestMethod1(Int32) Widzimy, że metoda rozszerzona została wywołana dla przypadku tc.TestMethod1(ob); gdyż tylko dla niego nie dało się dopasować metody w klasie TestClass. Mam nadziej, że tutaj wszystko jest jasne. W przypadku gdy brak dopasowania po stronie klasy, dopasowanie po stronie metod rozszerzonych odbywa się identycznie. Przykład:
namespace TestApp
{
   public class TestClass
   {
       public void TestMethod1()
       {
           System.Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod());
       }

       public static implicit operator int(TestClass tc)
       {
           System.Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod());
           return 12;
       }

       //public void TestMethod1(int x)
       //{
       //    System.Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod());
       //}

       //public void TestMethod1(object x)
       //{
       //    System.Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod());
       //}   
   }

   public static class Extensions
   {
       public static void TestMethod1(this TestClass tc)
       {  
           System.Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod());
       }

       //public static void TestMethod1(this TestClass tc, int x)
       //{  
       //    System.Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod());
       //}

       public static void TestMethod1(this TestClass tc, object x)
       {  
           System.Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod());
       }

       public static void Test2()
       {
           TestClass tc = new TestClass();
           object ob = 5;
           int x = 5;

           tc.TestMethod1();
           tc.TestMethod1(tc);
           tc.TestMethod1(ob);
           tc.TestMethod1(x);
       }
   }

   public partial class Form1 : Form
   {
       public Form1()
       {
           InitializeComponent();
           Extensions.Test2();
       }
   }
}
Wynik: Void TestMethod1() Void TestMethod1(TestApp.TestClass, System.Object) Void TestMethod1(TestApp.TestClass, System.Object) Void TestMethod1(TestApp.TestClass, System.Object) Całą sprawę można jeszcze bardziej skomplikować dodając dziedziczenie obiektów i interfejsów. Wkrótce powinienem się tym dokładnie zająć. Uzupełnienie o .NET 4.0: parametry domyślne także mogą być używane w metodach rozszerzających. Teraz rozpatrzmy dla jakich elementów możemy napisać metodę rozszerzoną i jak tutaj kompilator ustala jakie metody rozszerzone można wywołać dla jakiego elementu. Przykład:
public static class Extensions
{
   public static void TestMethod11(this int tc)
   {
       System.Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod());
   }

   public static void TestMethod12(this object tc)
   {
       System.Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod());
   }

   public static void TestMethod21(this int[] tc)
   {
       System.Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod());
   }

   public static void TestMethod22(this object[] tc)
   {
       System.Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod());
   }

   public static void TestMethod31(this List<int> tc)
   {
       System.Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod());
   }

   public static void TestMethod32(this List<object> tc)
   {
       System.Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod());
   }

   public static void TestMethod41(this Enum tc)
   {
       System.Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod());
   }

   public static void TestMethod42(this TestEnum tc)
   {
       System.Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod());
   }

   public static void TestMethod51(this TestStruct tc)
   {
       System.Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod());
   }

   public static void TestMethod52(this ValueType tc)
   {
       System.Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod());
   }

   public enum TestEnum
   {
       er,
       ty,
       ui
   };

   public enum TestEnum1
   {
       er,
       ty,
       ui
   };

   public struct TestStruct
   {
   }

   public static void Test2()
   {
       int int_val = 5;
       object obj_val = 5;

       int_val.TestMethod11();
       int_val.TestMethod12();
       int_val.TestMethod52()
       obj_val.TestMethod12();

       int[] int_ar = { 4, 5, 6 };
       object[] obj_ar = { 4, 5, 6 };

       //int_ar.TestMethod12();
       int_ar.TestMethod21();
       //obj_ar.TestMethod12();
       obj_ar.TestMethod22();

       List<object> li = new List<object> { 4, 5, 6 };
       List<int> int_list = new List<int> { 4, 5, 6 };

       //li.TestMethod12();
       li.TestMethod32();
       //int_list.TestMethod12();
       int_list.TestMethod31();

       TestEnum te = TestEnum.er;
       TestEnum1 te1 = TestEnum1.er;

       //te.TestMethod12();
       te.TestMethod41();
       te.TestMethod42();
       te.TestMethod52();
       //te1.TestMethod12();
       te1.TestMethod41();
       te.TestMethod52();

       TestStruct ts;

       //ts.TestMethod12();
       ts.TestMethod51();
       ts.TestMethod52();
   }
}
Zauważmy też że int[] nie jest jednocześnie obj[], tak jak int jest jednocześnie obj, to samo tyczy się listy. Pozatym widzimy, że struct dziedziczy po ValueType, enum po Enum, zaś Enum po ValueType. Pozostała nam do zbadania ostatnia sprawa. Co się dzieje jak istnieją dwie takie same metody rozszerzone w dwóch różnych przestrzeniach nazw:
using TestApp1;
using TestApp2;

namespace TestApp1
{
   public static class Extensions1
   {
       public static void Test(this int t)
       {
           System.Console.WriteLine("TestApp1.Extensions1");
       }
   }

   public static class Extensions2
   {
       public static void Test(this int t)
       {
           System.Console.WriteLine("TestApp1.Extensions2");
       }
   }
}

namespace TestApp2
{
   public static class Extensions1
   {
       public static void Test(this int t)
       {
           System.Console.WriteLine("TestApp2.Extensions1");
       }
   }

   public static class Extensions2
   {
       public static void Test(this int t)
       {
           System.Console.WriteLine("TestApp2.Extensions2");
       }
   }
}

namespace TestApp
{
   public static class Extensions1
   {
       public static void Test(this int t)
       {
           System.Console.WriteLine("TestApp.Extensions1");
       }
   }

   public static class Extensions2
   {
       public static void Test(this int t)
       {
           System.Console.WriteLine("TestApp.Extensions2");
       }
   }

   public partial class Form1 : Form
   {
       public Form1()
       {
           InitializeComponent();
           Test2();
       }

       private void Test2()
       {
           int v = 5;

           v.Test();
       }
   }
}
W takim wypadku dostaniemy błąd typu: The call is ambiguous between the following methods or properties: 'TestApp.Extensions1.Test(int)' and 'TestApp.Extensions2.Test(int)' Po jego usunięciu i zakomentowaniu metody TestApp.Extensions2.Test() kod skompiluje się, a wywołana zostanie metoda TestApp.Extensions1.Test(). Czyli możemy powiedzieć, że metody rozszerzone z przestrzeni nazw, z której tą metodę rozszerzoną wywołujemy mają pierwszeństwo nad innymi metodami rozszerzonymi z innych przestrzeni nazw. Komentujemy metodę TestApp.Extensions1.Test() i mamy błąd: The call is ambiguous between the following methods or properties: 'TestApp1.Extensions1.Test(int)' and 'TestApp1.Extensions2.Test(int)'. Komentujemy metodę TestApp1.Extensions2.Test() i mamy błąd: The call is ambiguous between the following methods or properties: 'TestApp1.Extensions1.Test(int)' and 'TestApp2.Extensions1.Test(int)' Zakomentujmy metodę TestApp2.Extensions1.Test() i mamy błąd: The call is ambiguous between the following methods or properties: 'TestApp2.Extensions2.Test(int)' and 'TestApp1.Extensions1.Test(int)' Czyli możemy powiedzieć, że jeśli dwie takie same metody rozszerzające są zdefiniowane w dwóch różnych przestrzeniach nazw lub takiej samej przestrzeni nazw, te przestrzenie nazw są różne od przestrzeni nazw z których je wywołujemy, to kompilator zgłasza błąd. Istnienie takich metod jest jak najbardziej poprawne. Dopiero ich użycie generuje błąd kompilatora. Jednym sposobem na usunięcie błędów jest zakomentowanie np. using TestApp2;. Drugim jest jawne wywołanie takiej metody: TestApp1.Extensions1.Test(5); Taka konstrukcja jest poprawna. Jak to wygląda od strony IL. Oto metoda Test2():
private void Test2()
{
   5.Test();
}
.method private hidebysig instance void Test2() cil managed
{
   .maxstack 1
   .locals init (
       [0] int32 v)
   L_0000: nop
   L_0001: ldc.i4.5
   L_0002: stloc.0
   L_0003: ldloc.0
   L_0004: call void TestApp.Extensions1::Test(int32)
   L_0009: nop
   L_000a: ret
}
Jak widać nie ma tutaj żadnego pakowania i rozpakowywania typów prostych. Tutaj trochę się zmieszałem, bo przynajmniej u mnie kiedy wpisuje 5 i kropka to nie pojawiają się żadne podpowiedzi, tak jakby zapis 5.Test(), czy 5.ToString() był niepoprawny. co sie dzieje dla int, czy on jest boxowany ?

Konstrukcja obiektów w C#, a ich widzialność

Pytanie brzmi czy w tworząc obiekt w jednym wątku możemy go ujrzeć w stanie częściowo zainicjalizowanym przez konstruktor z poziomu innego wątku. Dla programu jednowątkowego zobaczenie obiektu w stanie częściowo zainicjalizowanym mogło by mieć miejsce gdyby w trakcie jego konstrukcji pojawił się wyjątek. Rozpatrzmy taki przykład:
public partial class Form1 : Form
{
    public TestValue m_v;

    public class TestValue
    {
        protected int x = 5;
        public int y = 6;
        public int z;
        public readonly int p = 4;

        public TestValue(int _y, int _z)
        {
            y = _y;
            z = _z;
            X = 4;
            p = 5;
        }

        public TestValue(int _y, int _z, TestValue _v)
        {
            _v = this;
            y = _y;
            z = _z;
            X = 4;
        }

        public TestValue()
        {
            X = 5;
        }

        public int X 
        {
            get
            {
                return x;
            }

            set
            {
                x = value;
            }
        }
    }

    public Form1()
    {
        InitializeComponent();
        Test1();
        Test2();
        Test3();
        Test4();
    }

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

    private void Test1()
    {
        m_v = new TestValue(3, 4);
    }

    private void Test2()
    {
        m_v = new TestValue();
    }

    private void Test3()
    {
        m_v = new TestValue() { X = 4, y = 6 };
    }

    private void Test4()
    {
        m_v = new TestValue(3, 4) { X = 5 };
    }
}
Tutaj możemy dopowiedzieć, że przyspieszony sposób konstrukcji obiektów, zastosowany np. w Test3() nie pozwala na inicjalizację zmiennych nie widocznych na zewnątrz klasy, a także tylko do odczytu. Dopuszczenie takiej możliwości naruszało by zasady enkapsulacji kodu. Zapis typu m_v = new TestValue() { X = 4, y = 6 }; nie tworzy nowego konstruktora. To wywołanie konstruktora plus uzupełnienie pół wartościami. Jak to wygląda z poziomu IL:
.method private hidebysig instance void Test1() cil managed
{
    .maxstack 8
    L_0000: nop 
    L_0001: ldarg.0 
    L_0002: ldc.i4.3 
    L_0003: ldc.i4.4 
    L_0004: newobj instance void TestApp.Form1/TestValue::.ctor(int32, int32)
    L_0009: stfld class TestApp.Form1/TestValue TestApp.Form1::m_v
    L_000e: ret 
}

.method private hidebysig instance void Test2() cil managed
{
    .maxstack 8
    L_0000: nop 
    L_0001: ldarg.0 
    L_0002: newobj instance void TestApp.Form1/TestValue::.ctor()
    L_0007: stfld class TestApp.Form1/TestValue TestApp.Form1::m_v
    L_000c: ret 
}

.method private hidebysig instance void Test3() cil managed
{
    .maxstack 3
    .locals init (
        [0] class TestApp.Form1/TestValue <>g__initLocal0)
    L_0000: nop 
    L_0001: ldarg.0 
    L_0002: newobj instance void TestApp.Form1/TestValue::.ctor()
    L_0007: stloc.0 
    L_0008: ldloc.0 
    L_0009: ldc.i4.4 
    L_000a: callvirt instance void TestApp.Form1/TestValue::set_X(int32)
    L_000f: nop 
    L_0010: ldloc.0 
    L_0011: ldc.i4.6 
    L_0012: stfld int32 TestApp.Form1/TestValue::y
    L_0017: ldloc.0 
    L_0018: stfld class TestApp.Form1/TestValue TestApp.Form1::m_v
    L_001d: ret 
}

.method private hidebysig instance void Test4() cil managed
{
    .maxstack 4
    .locals init (
        [0] class TestApp.Form1/TestValue <>g__initLocal1)
    L_0000: nop 
    L_0001: ldarg.0 
    L_0002: ldc.i4.3 
    L_0003: ldc.i4.4 
    L_0004: newobj instance void TestApp.Form1/TestValue::.ctor(int32, int32)
    L_0009: stloc.0 
    L_000a: ldloc.0 
    L_000b: ldc.i4.5 
    L_000c: callvirt instance void TestApp.Form1/TestValue::set_X(int32)
    L_0011: nop 
    L_0012: ldloc.0 
    L_0013: stfld class TestApp.Form1/TestValue TestApp.Form1::m_v
    L_0018: ret 
}

.method private hidebysig instance void Test5() cil managed
{
    .maxstack 5
    .locals init (
        [0] class TestApp.Form1/TestValue <>g__initLocal2)
    L_0000: nop 
    L_0001: ldarg.0 
    L_0002: ldc.i4.3 
    L_0003: ldc.i4.4 
    L_0004: ldarg.0 
    L_0005: ldfld class TestApp.Form1/TestValue TestApp.Form1::m_v
    L_000a: newobj instance void TestApp.Form1/TestValue::.ctor(int32, int32, class TestApp.Form1/TestValue)
    L_000f: stloc.0 
    L_0010: ldloc.0 
    L_0011: ldc.i4.5 
    L_0012: callvirt instance void TestApp.Form1/TestValue::set_X(int32)
    L_0017: nop 
    L_0018: ldloc.0 
    L_0019: stfld class TestApp.Form1/TestValue TestApp.Form1::m_v
    L_001e: ret 
}
Najważniejszy wniosek jaki z kodu IL płynie, to taki, że referencja do obiektu jest ustawiana po pełnej konstrukcji obiektu. Dotyczy to zwłaszcza uproszczonej inicjalizacji obiektu, przy której najpierw wywoływany jest konstruktor, później ustawiane są pola obiektu, a na samym końcu ustawiana jest zmienna będąca referencją do obiektu. Czyli konstrukcje takie jak w metodzie Test3() zapewniają nam atomowość konstrukcji obiektu, zarówno jeśli chodzi o możliwość pojawienia się wyjątków w trakcie konstrukcji obiektów, jak i ich widoczności z innych wątków.

Wykorzystanie nazw generowanych przez kompilator.

Mamy taki kod:
public partial class Form1 : Form
{
    public class TestValue
    {
        public int v { get; set; }

        public static TestValue operator +(TestValue l, TestValue r)
        {
            return new TestValue() { v = l.v + r.v };
        }
    }

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

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

    private void Test1()
    {
        Func<int> del = () => { return 12; };

        TestValue v1 = new TestValue();
        v1.v = 5;
        TestValue v2 = new TestValue() { v = 5 };
        TestValue v3 = v1 + v2;
    }
}
Po stronie IL wygląda to tak:
.method private hidebysig instance void Test1() cil managed
{
    .maxstack 3
    .locals init (
        [0] class [System.Core]System.Func`1<int32> del,
        [1] class TestApp.Form1/TestValue v1,
        [2] class TestApp.Form1/TestValue v2,
        [3] class TestApp.Form1/TestValue v3,
        [4] class TestApp.Form1/TestValue <>g__initLocal0)
    L_0000: nop 
    L_0001: ldsfld class [System.Core]System.Func`1<int32> TestApp.Form1::CS$<>9__CachedAnonymousMethodDelegate2
    L_0006: brtrue.s L_001b
    L_0008: ldnull 
    L_0009: ldftn int32 TestApp.Form1::<Test1>b__1()
    L_000f: newobj instance void [System.Core]System.Func`1<int32>::.ctor(object, native int)
    L_0014: stsfld class [System.Core]System.Func`1<int32> TestApp.Form1::CS$<>9__CachedAnonymousMethodDelegate2
    L_0019: br.s L_001b
    L_001b: ldsfld class [System.Core]System.Func`1<int32> TestApp.Form1::CS$<>9__CachedAnonymousMethodDelegate2
    L_0020: stloc.0 
    L_0021: newobj instance void TestApp.Form1/TestValue::.ctor()
    L_0026: stloc.1 
    L_0027: ldloc.1 
    L_0028: ldc.i4.5 
    L_0029: callvirt instance void TestApp.Form1/TestValue::set_v(int32)
    L_002e: nop 
    L_002f: newobj instance void TestApp.Form1/TestValue::.ctor()
    L_0034: stloc.s <>g__initLocal0
    L_0036: ldloc.s <>g__initLocal0
    L_0038: ldc.i4.5 
    L_0039: callvirt instance void TestApp.Form1/TestValue::set_v(int32)
    L_003e: nop 
    L_003f: ldloc.s <>g__initLocal0
    L_0041: stloc.2 
    L_0042: ldloc.1 
    L_0043: ldloc.2 
    L_0044: call class TestApp.Form1/TestValue TestApp.Form1/TestValue::op_Addition(class TestApp.Form1/TestValue, class TestApp.Form1/TestValue)
    L_0049: stloc.3 
    L_004a: ret 
}
Jak widać dla obsłużenia metod anonimowych kompilator generuje kilka metod. Gdybyśmy we wnętrzu metody anonimowej skorzystali ze zmiennych spoza jej zasięgu były by to klasy. Tych nazw nie możemy wykorzystać w naszym kodzie gdyż nie jesteśmy w stanie zadeklarować zmiennych o nazwach typu CS$<>9__CachedAnonymousMethodDelegate3, <>g__initLocal0, <Test1>b__2. Pozostaje nam kwestia metod typu op_Addition() i set_v(). Bezpośrednie ich użycie w kodzie spowoduje błąd kompilacji: cannot explicitly call operator or accessor Zaś zadeklarowanie metod o takich nazwach spowoduje błąd: Type 'TestValue' already reserves a member called 'set_v' with the same parameter types Ostatecznie gdyby nam się udało w jakimś innym przypadku użyć nazwa automatycznie generowanych przez kompilator, to nic nie stoi na przeszkodzie by kompilator wygenerował inne nazwy. W tym momencie nie mam wiedzy czy takie coś się zdarza. Dlaczego więc to nie działa dla nazw metod operatorów i właściwości. Obiekt skompilowany do kodu pośredniego może zostać wykorzystany np. jako assembly. Nikt nam nie broni nadpisać takiego obiektu. No i stajemy przed tym samym problemem, że nie możemy stworzyć funkcji o nazwie set_v, bo takowa już istnieje w sposób niejawny. Można by to pewnie obejść, ale to jest taka walka z problemami, które sami tworzymy. Najlepiej od początku zabronić używania takich nazw i zastrzec je. Być może istnieją jeszcze inne powody, ale na tą chwilę nic innego nie potrafię wymyślić.

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.

Opóźnione wykonanie w LINQ

Weźmy pod uwagę taki przykład:
public int Value()
{
    System.Console.WriteLine("Value()");
    return new System.Random().Next(10);
}

private void Test1()
{
    System.Console.WriteLine("step 1");
    var x2 = new[] { Value(), Value(), Value(), Value(), Value() };
    System.Console.WriteLine("step 2");
    var x3 = from v in x2 select v + Value();
    System.Console.WriteLine("step 3");
    var x4 = x3.ToList();
    System.Console.WriteLine("step 4");
    var x5 = x3.ToList();
}
Wynik działania kodu: step 1 Value() Value() Value() Value() Value() step 2 step 3 Value() Value() Value() Value() Value() step 4 Value() Value() Value() Value() Value() Widzimy, że wywoływanie funkcji Value(), z czym wiąże się ewaluacja wartości zmiennej, następuje w kroku 1, 3, 4. Nie następuje w kroku 2. Niewykonanie kodu związanego z krokiem 2 to właśnie opóźnione wykonanie, w przypadku gdyby krok 3 i 4 został pominięty wykonanie kodu w kroku 2 w ogóle by nie nastąpiło. Poza tym zauważmy, że w kroku 3 i 4 następuje następuje dwukrotne jakby wygenerowanie zmiennej 3. Czyli opóźnione wykonanie nie oznacza opóźnionej, ale jednokrotnej ewaluacji zmiennej. Opóźnione wykonanie nie jest żadną magią. Najpierw powinniśmy zrozumieć co kryje się pod zmienną typu var. Nie jest to żaden dowolny typ danych jakiego moglibyśmy się spodziewać np. w językach skryptowych. Typ zmiennej var jest jednoznacznie ustalany przez kompilator na podstawie tego, co zwraca dane wyrażenie. Jakiego typu są więc zmienne w naszym przypadku. Oto wynik dekompilacji z Reflectora:
private void Test1()
{
    Console.WriteLine("step 1");
    int[] x2 = new int[] { this.Value(), this.Value(), this.Value(), this.Value(), this.Value() };
    Console.WriteLine("step 2");
    IEnumerable<int> x3 = x2.Select<int, int>(delegate (int v) {
        return v + this.Value();
    });
    Console.WriteLine("step 3");
    List<int> x4 = x3.ToList<int>();
    Console.WriteLine("step 4");
    List<int> x5 = x3.ToList<int>();
}
I tutaj w zasadzie mamy podane wyjaśnienie dlaczego podczas kroku 2 nic się nie stało i dlaczego podczas kroku 3 i 4 zmienna x3 została dwukrotnie ewaluowana. Jest ona typu IEnumerable. I to jest cała magia opóźnionego wykonania, dopóki nie zaczniemy dokonywać enumeracji dopóki nic się nie dzieje. Większość formuł LINQ zwraca właśnie enumeratory, stąd ich wykonanie jest opóźnione. Często zdarza się, że formuły LINQ budują swoistą kaskadę i że, całe przetwarzanie jest inicjowane przez ostatnią z nich. Przykładowe operacje, które powodują enumeracje: pobranie sumy elementów, ilości elementów, wartości konkretnego elementu, konwersja do listy. Nie zawsze wszystkie elementy są enumerowane: w przypadku np. funkcji Contains() enumeracja zatrzyma się na pierwszym elemencie, który spełnia warunek, optymistycznie pierwszym, pesymistycznie po ostatnim. Kompilator nie czyni żadnych założeń co do niezmienności danych. I choć zapis formuły LINQ przypomina zapis polecenia SQL, to jednak w przeciwieństwie do SQL żadne optymalizacje związane z nieprzetwarzaniem dwa razy tego samego (np. podwójnie zagnieżdżony select) nie są w LINQ robione.
private void Test1()
{
    var x2 = new[] { Value(), Value(), Value(), Value(), Value() };
    var x3 = from v in x2 select v + Value();
    var x4 = from v in x3 select x3.Sum() + v;
    x4.ToList();
}
Podczas wykonywania x4.ToList() funkcja Value() zostanie wywołana 30 razy. Trzeba na to zwrócić uwagę. LINQ samo w sobie jest wolne, a dzięki efektom takim jak wyżej możemy je spowolnić dodatkowo. Trzeba pamiętać, że x3 nie jest zmienną posiadając jakiś stan, jest enumeratorem i każde jej użycie powoduje enumerację. Trochę może się to kłócić z naszą percepcją. Poza tym zauważmy, że ponieważ funkcja Value() zwraca wartość losową, to każde wykonanie x3.Sum() da inny wynik. Wystarczy trochę zmienić kod:
private void Test1()
{
    var x2 = new[] { Value(), Value(), Value(), Value(), Value() };
    var x3 = (from v in x2 select v + Value()).ToList();
    var x4 = from v in x3 select x3.Sum() + v;
    x4.ToList();
} 
I liczba wywołań Value() maleje o 25. Często mówi się, że wywołanie ToList() powoduje natychmiastowe wyliczenie wartości obiektu, tymczasem oznacza to, że zmienna typu var nie będzie typu IEnumerable, a typu List, co powoduje, że musi ona być wyliczona.