Tuesday, March 11, 2008

Virtual, new and override - the differences

Auf die Frage was ist der Unterschied zwischen new und override, im konkreten Fall für ein Property verwendet, bekommt man stehts die korrekte Antwort. New versteckt das Property in der Basisklasse, override überschreibt dieses. Gut, aber was heisst das genau? Nun werden die sinnvollen Antworten doch eher rare.

Folgendes Beispiel zeigt die Funktionsweisen auf. Parent ist die Basisklasse, die zwei Properties Foo und Bar anbietet. Child ist eine Klasse die von Parent ableitet und die beiden Properties einmal mit new und einmal mit override überschreibt. In der Klasse MyClass werden zwei Instanzen von Child erzeugt, wobei im ersten Fall die Deklaration auf die Basisklasse Parent lautet. Die beiden Properties der beiden Instancen werden abgefragt und ausgedruckt.

public class MyClass
{
public static void Main()
{
Parent p = new Child();
Child c = new Child();
Console.WriteLine(p.Foo);
Console.WriteLine(p.Bar);
Console.WriteLine(c.Foo);
Console.WriteLine(c.Bar);
Console.ReadLine();
}
}
public class Parent
{
public string Foo {get{return "ParentFoo";}}
public virtual string Bar {get{return "ParentBar";}}
}
public class Child : Parent
{
public new string Foo {get{return "ChildFoo";}}
public override string Bar {get{return "ChildBar";}}
}

Das aufschlussreiche Ergebniss sieht so aus:
ParentFoo
ChildBar
ChildFoo
ChildBar

Da zwei Instancen von Child erzeugt wurden ist der Ausdruck ParentFoo doch für den einen oder anderen erstaunlich. Wie kommt das? Die Antwort liegt im Early- und Late Binding.

Im Fall von Foo wurde das Property auf der Parent Klasse nicht mit virtual deklariert. Somit wird das Early Binding verwendet. Das heisst, der Compiler legt zur Kompilierzeit fest welches Property aufgerufen wird. Da im ersten Fall die Variable als Parent deklariert ist, wird in jedem Fall (egal was für eine Instanz effektiv vorhanden ist) das Property Foo der Klasse Parent aufgerufen und im zweiten Fall das Property der Klasse Child.

Im Fall von Bar wurde das Property auf der Basisklasse mit virtual deklariert und somit das Early Binding verhindert. Das heisst, das zur Laufzeit die Instance analysiert wird und entsprechend das Property der effektiv vorhandenen Klasse (in unserem Fall Child) aufgerufen wird.

Und zum Schluss noch einen Blick auf den IL Code:

.method public hidebysig static void Main() cil managed
{
.entrypoint
.maxstack 1
.locals init (
[0] class Parent parent,
[1] class Child child)
L_0000: newobj instance void Child::.ctor()
L_0005: stloc.0
L_0006: newobj instance void Child::.ctor()
L_000b: stloc.1
L_000c: ldloc.0
L_000d: callvirt instance string Parent::get_Foo()
L_0012: call void [mscorlib]System.Console::WriteLine(string)
L_0017: ldloc.0
L_0018: callvirt instance string Parent::get_Bar()
L_001d: call void [mscorlib]System.Console::WriteLine(string)
L_0022: ldloc.1
L_0023: callvirt instance string Child::get_Foo()
L_0028: call void [mscorlib]System.Console::WriteLine(string)
L_002d: ldloc.1
L_002e: callvirt instance string Parent::get_Bar()
L_0033: call void [mscorlib]System.Console::WriteLine(string)
L_0038: call string [mscorlib]System.Console::ReadLine()
L_003d: pop
L_003e: ret
}

Würde man nicht erwarten, dass beim Early Binding ein call und nicht ein callvirt aufgerufen wird? Eigentlich schon. Das callvirt wird generiert, damit die Klasse auf null geprüft werden kann. Der JIT Compiler wird aber nach der null Prüfung diesen Aufruf effektiv in einen non-virtual Aufruf umsetzen.