2009-07-09

Dwa rodzaje rzutowania w C#

W C# możemy przeprowadzić rzutowanie na dwa sposoby - poprzez użycie nawiasów albo za pomocą słówka kluczowego as. Przykład zastosowania:
public class TestClass
{
}

public void Test1()
{
 {
     TestClass t = new TestClass();
     object o = t;
     t = (TestClass)o;
 }

 {
     TestClass t = new TestClass();
     object o = t;
     t = o as TestClass;
 }
}
Różnica w kodzie pośrednim jest następująca:
.method public hidebysig instance void Test1() cil managed
{
 .maxstack 1
 .locals init (
     [0] class TestAppNET.CastingTest/TestClass t,
     [1] object o)
 L_0000: nop
 L_0001: nop
 L_0002: newobj instance void TestAppNET.CastingTest/TestClass::.ctor()
 L_0007: stloc.0
 L_0008: ldloc.0
 L_0009: stloc.1
 L_000a: ldloc.1
 L_000b: castclass TestAppNET.CastingTest/TestClass
 L_0010: stloc.0
 L_0011: nop
 L_0012: nop
 L_0013: newobj instance void TestAppNET.CastingTest/TestClass::.ctor()
 L_0018: stloc.0
 L_0019: ldloc.0
 L_001a: stloc.1
 L_001b: ldloc.1
 L_001c: isinst TestAppNET.CastingTest/TestClass
 L_0021: stloc.0
 L_0022: nop
 L_0023: ret
}
Pierwsze podejście używa rozkazu castclass, drugie isinst. Różnica w działaniu Obie metody robią to samo, różnica jest w reakcji na nieprawidłowe rzutowanie:
public class TestClass1
{
}

public class TestClass2
{
}

public void Test2()
{
 {
     TestClass1 t = new TestClass1();
     object o = t;
     TestClass2 tt = o as TestClass2;
     Debug.Assert(tt == null);
 }

 {
     TestClass1 t = new TestClass1();
     object o = t;
     bool ex = false;
     try
     {
         TestClass2 tt = (TestClass2)o;
     }
     catch (InvalidCastException)
     {
         ex = true;
     }
     Debug.Assert(ex);
 }
}
Jak widzimy operator as zwraca null jeśli konwersja się nie powiedzie, zaś rzutowanie za pomocą nawiasów zwróci wyjątek InvalidCastException. Zastosowanie dla typów wartościowych W przypadku typów wartościowych (struktury, enumeracje, typy proste), które nie mogą przyjmować wartość null użycie pierwsze metody jest niedozwolone (ściślej tylko do konwersji obiekt -> typ wartościowy, konwersje na obiekt możemy przeprowadzić zawsze bez użycia jakiejkolwiek konwersji).
public void Test3()
{
 {
     int i = 5;
     object o = i;
     //i = o as int;
 }

 {
     int i = 5;
     object o = i;
     i = (int)o;
 }
}
W przypadku typów wartościowych jest to trochę zaciemnione. Konwersja typu wartościowego na klasę bazową oznacza tak naprawdę pakowanie typu wartościowego do stworzonej wewnętrznie przez kompilator klasy. Dopiero o tej klasie możemy powiedzieć, że np. w przypadku struktur dziedziczy po ValueType. Podobnie w drugą stronę, konwersja na typ wartościowy oznacza wypakowanie obiektu do typu wartościowego. Zauważmy też, że operatorów rzutowania używamy tylko jeśli konwertujemy obiekt klasy bazowej na klasę potomną. W drugą stronę konwersja nie jest wymagana (choć użycie operatorów konwersji nie jest błędem) gdyż kompilator wie, że jest ona jak najbardziej poprawna i wykonalna. Następująca modyfikacja pierwszego przykładu nie zmieni wyglądu generowanego kodu pośredniego. Zauważmy, że konwersja na typ bazowy to zwykłe skopiowanie referencji z jednej zmiennej do drugiej.
public void Test1a()
{
 {
     TestClass t = new TestClass();
     object o = (object)t;
     t = (TestClass)o;
 }

 {
     TestClass t = new TestClass();
     object o = t as object;
     t = o as TestClass;
 }
}
Jaka jest różnica w prędkości pomiędzy tymi dwoma podejściami ? Oczywiście test ten możemy tylko przeprowadzić dla obiektów.
public class TestClass3
{
 public int A;
}

public void Test4()
{
 TestClass3 t = new TestClass3();
 object o = t;
 const int loop = 1000;

 {
     Stopwatch sw = new Stopwatch();
     sw.Start();
     for (int i = 0; i < loop; i++)
     {
         TestClass3 tt = o as TestClass3;
         if (tt != null)
             tt.A++;
     }
     sw.Stop();
     System.Console.WriteLine(sw.ElapsedMilliseconds);
 }

 {
     Stopwatch sw = new Stopwatch();
     sw.Start();
     for (int i = 0; i < loop; i++)
     {
         TestClass3 tt = (TestClass3)o;
         if (tt != null)
             tt.A++;
     }
     sw.Stop();
     System.Console.WriteLine(sw.ElapsedMilliseconds);
 }

 {
     Stopwatch sw = new Stopwatch();
     sw.Start();
     for (int i = 0; i < loop; i++)
     {
         try
         {
             TestClass3 tt = (TestClass3)o;
             if (tt != null)
                 tt.A++;
         }
         catch (InvalidCastException)
         {
             Debug.Assert(false);
         }

     }
     sw.Stop();
     System.Console.WriteLine(sw.ElapsedMilliseconds);
 }

 {
     Stopwatch sw = new Stopwatch();
     sw.Start();
     for (int i = 0; i < loop; i++)
     {
         TestClass2 tt = o as TestClass2;
         if (tt == null)
             t.A++;
     }
     sw.Stop();
     System.Console.WriteLine(sw.ElapsedMilliseconds);
 }

 {
     Stopwatch sw = new Stopwatch();
     sw.Start();
     for (int i = 0; i < loop; i++)
     {
         try
         {
             TestClass1 tt = (TestClass1)o;
             Debug.Assert(true);
         }
         catch (InvalidCastException)
         {
             if (t != null)
                 t.A++;
         }
     }
     sw.Stop();
     System.Console.WriteLine(sw.ElapsedMilliseconds);
 }

 System.Console.WriteLine(t.A);
}
Wyniki dla zakomentowanego ostatniego pomiaru: 958 1045 992 1242 0 800000000 Odkomentowany ostatni pomiar, zmniejszono liczbę pętli do loop = 1000: 0 0 0 0 2567 5000 Widzimy, że pomiędzy tymi metodami nie ma zbytniej różnicy w prędkości. Dopóki rzutowanie nie wywoła wyjątku. Jeśli to ma być nasz sposób na sprawdzanie typu obiektu to lepiej z niego zrezygnować. Można powiedzieć, że detekcja niepoprawności typu za pomocą () jest 2500 razy wolniejsza niż za pomocą operatora as. Konwersje pomiędzy typami standardowymi
public void Test5()
{
 int i1 = 5;

 double d1 = 5;
 double d2 = 5.5;
 double d3 = i1;
 double d4 = (double)i1;

 int i2 = (int)5.5;
 int i3 = (int)d1;

 i1.ToString();
 i2.ToString();
 i3.ToString();
 d1.ToString();
 d2.ToString();
 d3.ToString();
 d4.ToString();
}
Kod pośredni:
.method public hidebysig instance void Test5() cil managed
{
 .maxstack 1
 .locals init (
     [0] int32 i1,
     [1] float64 d1,
     [2] float64 d2,
     [3] float64 d3,
     [4] float64 d4,
     [5] int32 i2,
     [6] int32 i3)
 L_0000: nop
 L_0001: ldc.i4.5
 L_0002: stloc.0
 L_0003: ldc.r8 5
 L_000c: stloc.1
 L_000d: ldc.r8 5.5
 L_0016: stloc.2
 L_0017: ldloc.0
 L_0018: conv.r8
 L_0019: stloc.3
 L_001a: ldloc.0
 L_001b: conv.r8
 L_001c: stloc.s d4
 L_001e: ldc.i4.5
 L_001f: stloc.s i2
 L_0021: ldloc.1
 L_0022: conv.i4
 L_0023: stloc.s i3
 L_0025: ldloca.s i1
 L_0027: call instance string [mscorlib]System.Int32::ToString()
 L_002c: pop
 L_002d: ldloca.s i2
 L_002f: call instance string [mscorlib]System.Int32::ToString()
 L_0034: pop
 L_0035: ldloca.s i3
 L_0037: call instance string [mscorlib]System.Int32::ToString()
 L_003c: pop
 L_003d: ldloca.s d1
 L_003f: call instance string [mscorlib]System.Double::ToString()
 L_0044: pop
 L_0045: ldloca.s d2
 L_0047: call instance string [mscorlib]System.Double::ToString()
 L_004c: pop
 L_004d: ldloca.s d3
 L_004f: call instance string [mscorlib]System.Double::ToString()
 L_0054: pop
 L_0055: ldloca.s d4
 L_0057: call instance string [mscorlib]System.Double::ToString()
 L_005c: pop
 L_005d: ret
}
Jak widać tutaj do konwersji używany jest rozkaz conv z różnymi sufiksami. Konwersje pośrednie:
public class TestClass4
{
 public static explicit operator TestClass4(int i)
 {
     return new TestClass4();
 }

 public static explicit operator  int(TestClass4 t)
 {
     return 11;
 }
}

public struct TestStruct
{
 public static explicit operator TestStruct(int i)
 {
     return new TestStruct();
 }

 public static explicit operator int(TestStruct t)
 {
     return 11;
 }
}

public void Test6()
{
 TestClass4 t1 = new TestClass4();
 int i1 = (int)t1;
 t1 = (TestClass4)i1;

 TestStruct t2 = new TestStruct();
 int i2 = (int)t2;
 t2 = (TestStruct)i2;
}
Kod pośredni:
.method public hidebysig instance void Test6() cil managed
{
 .maxstack 1
 .locals init (
     [0] class TestAppNET.CastingTest/TestClass4 t1,
     [1] int32 i1,
     [2] valuetype TestAppNET.CastingTest/TestStruct t2,
     [3] int32 i2)
 L_0000: nop
 L_0001: newobj instance void TestAppNET.CastingTest/TestClass4::.ctor()
 L_0006: stloc.0
 L_0007: ldloc.0
 L_0008: call int32 TestAppNET.CastingTest/TestClass4::op_Explicit(class TestAppNET.CastingTest/TestClass4)
 L_000d: stloc.1
 L_000e: ldloc.1
 L_000f: call class TestAppNET.CastingTest/TestClass4 TestAppNET.CastingTest/TestClass4::op_Explicit(int32)
 L_0014: stloc.0
 L_0015: ldloca.s t2
 L_0017: initobj TestAppNET.CastingTest/TestStruct
 L_001d: ldloc.2
 L_001e: call int32 TestAppNET.CastingTest/TestStruct::op_Explicit(valuetype TestAppNET.CastingTest/TestStruct)
 L_0023: stloc.3
 L_0024: ldloc.3
 L_0025: call valuetype TestAppNET.CastingTest/TestStruct TestAppNET.CastingTest/TestStruct::op_Explicit(int32)
 L_002a: stloc.2
 L_002b: ret
}
Jak widać podobieństwo jest tylko na poziomie składni. W kodzie pośrednim konwersja jest dokonywana zupełnie inaczej. Za pomocą operatora as nie da się przeprowadzić tego typu konwersji.