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.

2 comments:

hubethom said...

Hi Jean-Claude,

der IL-Code ist recht interessant.

Beim new-Keyword spricht man klassisch übrigens auch von Methoden-"Überdeckung". Es gibt demnach Überdecken (new), Überschreiben (override) und Überladen(gleicher Methodenname, andere Parameter).

Das new-Keyword ist in C# nicht zwingend notwendig. MS hat es lediglich eingeführt, damit wir nicht aus Versehen eine Methode überschreiben. Lässt man es weg, kommt nur eine Warning.

Die Java-Gemeinde hat meines wissens nur ein Überschreiben. Überdecken gibt's dort nur bei statischen Methoden.
Zum Überschreiben haben Sie das Keyword override auch als nützlich empfunden, damit in Subklassen nicht versehentlich eine Methode überschrieben wird. Allerdings besitzt Java kein override-Keyword für Methoden, sondern eine @override-Annotation, welche vor eine Methode geschrieben wird. (Annotation sind dort ähnlich wie .NET Attribute, lassen sich mit Reflection auslesen).

Jean-Claude Trachsel said...

Hallo Thomas

Gebe dir in allen Punkten recht.

Vergleiche zu Java habe ich in Gesprächen auch schon oft gemacht.
Betont haben wir kürzlich, dass es in Java nur ein Late Binding gibt. Somit braucht's keine Unterscheidung zwischen new und override. Wie stark sich das C# Early Binding wirklich auf eine bessere Performance auswirkt ist für mich aber (noch) nicht klar.