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 ?

Brak komentarzy:

Prześlij komentarz