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.