Da Sie gefragt haben, warum C # dies so gemacht hat, sollten Sie die C # -Ersteller fragen. Anders Hejlsberg, der Hauptarchitekt für C #, antwortete in einem Interview , warum er sich dafür entschieden hat, nicht standardmäßig auf virtuell umzusteigen (wie in Java) .
Beachten Sie, dass Java standardmäßig virtuell ist und das Schlüsselwort final verwendet , um eine Methode als nicht virtuell zu kennzeichnen. Es sind noch zwei Konzepte zu lernen, aber viele Leute kennen das endgültige Schlüsselwort nicht oder verwenden es nicht proaktiv. C # zwingt einen dazu, virtuell und neu / außer Kraft zu setzen, um diese Entscheidungen bewusst zu treffen.
Es gibt verschiedene Gründe. Eins ist Leistung . Wir können beobachten, dass Menschen beim Schreiben von Code in Java vergessen, ihre Methoden als endgültig zu kennzeichnen. Daher sind diese Methoden virtuell. Weil sie virtuell sind, arbeiten sie nicht so gut. Mit einer virtuellen Methode ist nur ein Performance-Overhead verbunden. Das ist ein Problem.
Ein wichtigeres Problem ist die Versionierung . Es gibt zwei Denkschulen zu virtuellen Methoden. Die akademische Denkschule sagt: "Alles sollte virtuell sein, weil ich es vielleicht eines Tages außer Kraft setzen möchte." Die pragmatische Denkweise, die sich aus der Erstellung realer Anwendungen ergibt, die in der realen Welt ausgeführt werden, lautet: "Wir müssen wirklich vorsichtig sein, was wir virtuell machen."
Wenn wir auf einer Plattform etwas virtuell machen, versprechen wir eine Menge, wie es sich in Zukunft entwickeln wird. Bei einer nicht virtuellen Methode versprechen wir, dass beim Aufrufen dieser Methode x und y auftreten. Wenn wir eine virtuelle Methode in einer API veröffentlichen, versprechen wir nicht nur, dass beim Aufrufen dieser Methode x und y auftreten werden. Wir versprechen auch, dass wenn Sie diese Methode überschreiben, wir sie in dieser bestimmten Reihenfolge in Bezug auf diese anderen aufrufen und der Zustand in dieser und jener Invariante sein wird.
Jedes Mal, wenn Sie in einer API "virtuell" sagen, erstellen Sie einen Rückruf-Hook. Als OS- oder API-Framework-Designer müssen Sie sehr vorsichtig sein. Sie möchten nicht, dass Benutzer an einem beliebigen Punkt in einer API überschreiben und Hooks erstellen, da Sie diese Versprechen nicht unbedingt einhalten können. Und die Leute verstehen möglicherweise nicht ganz, was sie versprechen, wenn sie etwas virtuell machen.
Das Interview enthält weitere Diskussionen darüber, wie Entwickler das Design der Klassenvererbung beurteilen und wie dies zu ihrer Entscheidung geführt hat.
Nun zur folgenden Frage:
Ich kann nicht verstehen, warum ich in meiner DerivedClass eine Methode mit demselben Namen und derselben Signatur wie BaseClass hinzufügen und ein neues Verhalten definieren werde, aber beim Laufzeit-Polymorphismus wird die BaseClass-Methode aufgerufen! (was nicht übergeordnet ist, aber logischerweise sollte es sein).
Dies ist der Fall, wenn eine abgeleitete Klasse deklarieren möchte, dass sie sich nicht an den Vertrag der Basisklasse hält, sondern über eine Methode mit demselben Namen verfügt. (Informationen zu allen Benutzern, die den Unterschied zwischen new
und override
in C # nicht kennen , finden Sie auf dieser MSDN-Seite. )
Ein sehr praktisches Szenario ist folgendes:
- Sie haben eine API mit einer Klasse namens erstellt
Vehicle
.
- Ich habe angefangen, Ihre API zu verwenden und abgeleitet
Vehicle
.
- Ihre
Vehicle
Klasse hatte keine Methode PerformEngineCheck()
.
- In meiner
Car
Klasse füge ich eine Methode hinzu PerformEngineCheck()
.
- Sie haben eine neue Version Ihrer API veröffentlicht und eine hinzugefügt
PerformEngineCheck()
.
- Ich kann meine Methode nicht umbenennen, da meine Clients von meiner API abhängig sind und dies zu Problemen führen würde.
Wenn ich also eine Neukompilierung mit Ihrer neuen API durchführe, warnt C # mich vor diesem Problem, z
Wenn die Basis PerformEngineCheck()
nicht war virtual
:
app2.cs(15,17): warning CS0108: 'Car.PerformEngineCheck()' hides inherited member 'Vehicle.PerformEngineCheck()'.
Use the new keyword if hiding was intended.
Und wenn die Basis PerformEngineCheck()
war virtual
:
app2.cs(15,17): warning CS0114: 'Car.PerformEngineCheck()' hides inherited member 'Vehicle.PerformEngineCheck()'.
To make the current member override that implementation, add the override keyword. Otherwise add the new keyword.
Jetzt muss ich explizit entscheiden, ob meine Klasse den Vertrag der Basisklasse tatsächlich verlängert oder ob es sich um einen anderen Vertrag handelt, der jedoch denselben Namen trägt.
- Dadurch
new
kann ich meine Clients nicht beschädigen, wenn die Funktionalität der Basismethode von der abgeleiteten Methode abweicht. Jeder Code, auf den verwiesen Vehicle
wird , wird nicht als Car.PerformEngineCheck()
aufgerufen angezeigt, aber Code, auf den verwiesen wurde, Car
zeigt weiterhin die gleiche Funktionalität an, die ich in angeboten habe PerformEngineCheck()
.
Ein ähnliches Beispiel ist, wenn eine andere Methode in der Basisklasse möglicherweise aufruft PerformEngineCheck()
(insbesondere in der neueren Version), wie verhindert man, dass sie die PerformEngineCheck()
der abgeleiteten Klasse aufruft ? In Java würde diese Entscheidung bei der Basisklasse liegen, sie weiß jedoch nichts über die abgeleitete Klasse. In C # beruht diese Entscheidung sowohl auf der Basisklasse (über das virtual
Schlüsselwort) als auch auf der abgeleiteten Klasse (über das Schlüsselwort new
und override
).
Natürlich bieten die Fehler, die der Compiler ausgibt, den Programmierern auch ein nützliches Werkzeug, um nicht unerwartet Fehler zu machen (dh entweder zu überschreiben oder neue Funktionen bereitzustellen, ohne dies zu bemerken).
Wie Anders sagte, zwingt uns die reale Welt zu solchen Problemen, auf die wir niemals eingehen wollen, wenn wir von vorne anfangen.
BEARBEITEN: Es wurde ein Beispiel hinzugefügt, wo new
die Schnittstellenkompatibilität sichergestellt werden soll.
BEARBEITEN: Während ich die Kommentare durchging, stieß ich auch auf einen Artikel von Eric Lippert (damals eines der Mitglieder des C # -Design-Komitees) über andere Beispielszenarien (von Brian erwähnt).
TEIL 2: Basierend auf einer aktualisierten Frage
Aber im Falle von C #, wenn SpaceShip die Fahrzeugklasse nicht überschreibt, wird die Logik meines Codes gebrochen. Ist das nicht ein Nachteil?
Wer entscheidet, ob SpaceShip
das tatsächlich außer Kraft gesetzt wird Vehicle.accelerate()
oder ob es anders ist? Es muss der SpaceShip
Entwickler sein. Wenn der SpaceShip
Entwickler also feststellt, dass der Vertrag der Basisklasse Vehicle.accelerate()
nicht eingehalten wird , sollte Ihr Anruf bei nicht erfolgen SpaceShip.accelerate()
, oder? Dann markieren sie es als new
. Wenn sie jedoch beschließen, dass der Vertrag tatsächlich eingehalten wird, werden sie ihn tatsächlich kennzeichnen override
. In beiden Fällen verhält sich Ihr Code korrekt, wenn Sie die auf dem Vertrag basierende Methode aufrufen . Wie kann Ihr Code entscheiden, ob er SpaceShip.accelerate()
tatsächlich überschreibt Vehicle.accelerate()
oder ob es sich um eine Namenskollision handelt? (Siehe mein Beispiel oben).
Im Falle einer impliziten Vererbung würde der Methodenaufruf jedoch immer noch an gehen , auch wenn SpaceShip.accelerate()
der Vertrag von nicht eingehalten wurde .Vehicle.accelerate()
SpaceShip.accelerate()