In Java gibt es keine virtual
, new
, override
Schlüsselwörter für Methodendefinition. Die Arbeitsweise einer Methode ist also leicht zu verstehen. Verursachen , wenn DerivedClass erstreckt Baseclass und hat ein Verfahren mit denselben Namen und dieselbe Signatur von Baseclass dann den übergeordneten findet im Laufzeit - Polymorphismus nehmen (vorausgesetzt , das Verfahren ist nicht static
).
BaseClass bcdc = new DerivedClass();
bcdc.doSomething() // will invoke DerivedClass's doSomething method.
Nun kommen Sie zu C #, es kann so viel Verwirrung und schwer zu verstehen sein, wie die new
oder virtual+derive
oder neue + virtuelle Überschreibung funktioniert.
Ich kann nicht verstehen, warum in der Welt ich eine Methode DerivedClass
mit demselben Namen und derselben Signatur wie hinzufügen BaseClass
und ein neues Verhalten definieren werde, aber beim Laufzeit-Polymorphismus wird die BaseClass
Methode aufgerufen! (was nicht übergeordnet ist, aber logischerweise sollte es sein).
Im Fall virtual + override
wenn die logische Implementierung ist richtig, aber der Programmierer zu denken hat , welche Methode er die Erlaubnis zum Benutzer zum Zeitpunkt der Codierung außer Kraft zu setzen geben soll. Welches hat einige Pro-Contra (gehen wir jetzt nicht dorthin).
Warum also in C # gibt es so viel Platz für un -logical Argumentation und Verwirrung. Darf ich also meine Frage neu formulieren, in welchem Kontext der realen Welt ich an die Verwendung virtual + override
anstelle von new
und auch an die Verwendung new
anstelle von denken soll virtual + override
?
Nach einigen sehr guten Antworten, insbesondere von Omar , habe ich erfahren, dass C # -Designer mehr Wert darauf legen, dass Programmierer überlegen sollten, bevor sie eine Methode entwickeln, die gut ist und mit einigen Anfängerfehlern aus Java umgeht.
Jetzt habe ich eine Frage im Kopf. Wie in Java, wenn ich einen Code wie hatte
Vehicle vehicle = new Car();
vehicle.accelerate();
und später mache ich neue klasse SpaceShip
abgeleitet von Vehicle
. Dann möchte ich alles car
in ein SpaceShip
Objekt ändern. Ich muss nur eine Codezeile ändern
Vehicle vehicle = new SpaceShip();
vehicle.accelerate();
Dies wird meine Logik an keiner Stelle des Codes brechen.
Aber im Falle von C #, wenn SpaceShip
nicht die Vehicle
Klasse überschreibt accelerate
und new
dann die Logik meines Codes verwendet, wird gebrochen. Ist das nicht ein Nachteil?
quelle
final
; es ist genau umgekehrt).@Override
.Antworten:
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.
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:
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
undoverride
in C # nicht kennen , finden Sie auf dieser MSDN-Seite. )Ein sehr praktisches Szenario ist folgendes:
Vehicle
.Vehicle
.Vehicle
Klasse hatte keine MethodePerformEngineCheck()
.Car
Klasse füge ich eine Methode hinzuPerformEngineCheck()
.PerformEngineCheck()
.Wenn ich also eine Neukompilierung mit Ihrer neuen API durchführe, warnt C # mich vor diesem Problem, z
Wenn die Basis
PerformEngineCheck()
nicht warvirtual
:Und wenn die Basis
PerformEngineCheck()
warvirtual
: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.
new
kann ich meine Clients nicht beschädigen, wenn die Funktionalität der Basismethode von der abgeleiteten Methode abweicht. Jeder Code, auf den verwiesenVehicle
wird , wird nicht alsCar.PerformEngineCheck()
aufgerufen angezeigt, aber Code, auf den verwiesen wurde,Car
zeigt weiterhin die gleiche Funktionalität an, die ich in angeboten habePerformEngineCheck()
.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 diePerformEngineCheck()
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 dasvirtual
Schlüsselwort) als auch auf der abgeleiteten Klasse (über das Schlüsselwortnew
undoverride
).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
Wer entscheidet, ob
SpaceShip
das tatsächlich außer Kraft gesetzt wirdVehicle.accelerate()
oder ob es anders ist? Es muss derSpaceShip
Entwickler sein. Wenn derSpaceShip
Entwickler also feststellt, dass der Vertrag der BasisklasseVehicle.accelerate()
nicht eingehalten wird , sollte Ihr Anruf bei nicht erfolgenSpaceShip.accelerate()
, oder? Dann markieren sie es alsnew
. Wenn sie jedoch beschließen, dass der Vertrag tatsächlich eingehalten wird, werden sie ihn tatsächlich kennzeichnenoverride
. 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 erSpaceShip.accelerate()
tatsächlich überschreibtVehicle.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()
quelle
Es wurde gemacht, weil es das Richtige ist. Tatsache ist, dass es falsch ist, zuzulassen, dass alle Methoden außer Kraft gesetzt werden. Dies führt zu dem fragilen Basisklassenproblem, bei dem Sie nicht sagen können, ob eine Änderung der Basisklasse zu einer Unterbrechung der Unterklassen führt. Daher müssen Sie entweder die Methoden, die nicht überschrieben werden sollen, auf die schwarze Liste setzen oder die Methoden auf die weiße Liste setzen, die überschrieben werden dürfen. Von den beiden ist Whitelisting nicht nur sicherer (da Sie nicht eine fragile Basisklasse erstellen können versehentlich ), es erfordert auch weniger Arbeit , da Sie die Vererbung für Komposition vermeiden sollten.
quelle
final
Modifikator. Wo liegt also das Problem?HashMap
du dich ?). Entweder für die Vererbung entwerfen und den Vertrag klarstellen oder nicht und sicherstellen, dass niemand die Klasse erben kann.@Override
wurde als Bandaid hinzugefügt, da es zu spät war, um das Verhalten zu ändern, aber selbst die ursprünglichen Java-Entwickler (insbesondere Josh Bloch) waren sich einig, dass dies eine schlechte Entscheidung war.Wie Robert Harvey sagte, ist alles in dem, was Sie gewohnt sind. Ich finde Javas Mangel an dieser Flexibilität seltsam.
Das heißt, warum haben diese überhaupt? Aus dem gleichen Grunde , dass C # hat
public
,internal
(auch „nichts“),protected
,protected internal
, undprivate
, aber Java nur hatpublic
,protected
nichts, undprivate
. Es bietet eine genauere Kontrolle über das Verhalten der von Ihnen programmierten Elemente, ohne dass Sie mehr Begriffe und Schlüsselwörter benötigen, um den Überblick zu behalten.Im Fall von
new
vs.virtual+override
geht es ungefähr so:abstract
undoverride
in der Unterklasse.virtual
undoverride
in der Unterklasse.new
in der Unterklasse.sealed
in der Basisklasse.Ein Beispiel aus der Praxis: Ein Projekt, an dem ich E-Commerce-Bestellungen aus vielen verschiedenen Quellen bearbeitet habe. Es gab eine Basis ,
OrderProcessor
die den größten Teil der Logik hatte, mit bestimmtenabstract
/virtual
Methoden für jedes Kind Klasse Quelle außer Kraft zu setzen. Dies funktionierte einwandfrei, bis wir eine neue Quelle erhielten, die eine völlig andere Art der Auftragsabwicklung hatte, sodass wir eine Kernfunktion ersetzen mussten. Zu diesem Zeitpunkt hatten wir zwei Möglichkeiten: 1)virtual
Zur Basismethode hinzufügen undoverride
im Kind; oder 2)new
dem Kind hinzufügen .Während einer von beiden arbeiten könnte , würde der erste es sehr einfach machen, diese bestimmte Methode in Zukunft wieder zu überschreiben. Es würde zum Beispiel in Auto-Vervollständigung angezeigt. Dies war jedoch ein Ausnahmefall, weshalb wir uns für die Verwendung entschieden haben
new
. Dadurch wurde der Standard von "Diese Methode muss nicht überschrieben werden" beibehalten, während der spezielle Fall berücksichtigt wurde, in dem dies der Fall war. Es ist ein semantischer Unterschied, der das Leben leichter macht.Beachten Sie jedoch, dass damit ein Verhaltensunterschied verbunden ist, nicht nur ein semantischer Unterschied. Einzelheiten finden Sie in diesem Artikel . Ich bin jedoch nie in eine Situation geraten, in der ich dieses Verhalten ausnutzen musste.
quelle
new
dieses Weges ein Fehler ist, der darauf wartet, passiert zu werden. Wenn ich eine Methode für eine Instanz aufrufe, erwarte ich, dass sie immer dasselbe ausführt, auch wenn ich die Instanz in ihre Basisklasse umwandle. Aber sonew
verhalten sich ed-Methoden nicht.new
ist in WebViewPage <TModel> im MVC-Framework. Aber ich bin auchnew
stundenlang von einem Bug befallen , deshalb halte ich es nicht für eine unvernünftige Frage.Das Design von Java ist so, dass bei jedem Verweis auf ein Objekt ein Aufruf eines bestimmten Methodennamens mit bestimmten Parametertypen, sofern dies überhaupt zulässig ist, immer dieselbe Methode aufruft. Es ist möglich, dass implizite Parametertypkonvertierungen durch den Typ einer Referenz beeinflusst werden, aber sobald alle derartigen Konvertierungen aufgelöst wurden, ist der Typ der Referenz irrelevant.
Dies vereinfacht die Laufzeit, kann jedoch einige unglückliche Probleme verursachen. Angenommen,
GrafBase
es wird nicht implementiertvoid DrawParallelogram(int x1,int y1, int x2,int y2, int x3,int y3)
, sondernGrafDerived
es wird als öffentliche Methode implementiert , die ein Parallelogramm zeichnet, dessen berechneter vierter Punkt dem ersten entgegengesetzt ist. Angenommen, eine spätere Version vonGrafBase
implementiert eine öffentliche Methode mit derselben Signatur, jedoch mit dem berechneten vierten Punkt gegenüber dem zweiten. Clients, die erwarten, dass a,GrafBase
aber eine Referenz auf a erhalten,GrafDerived
werdenDrawParallelogram
den vierten Punkt in der Art und Weise der neuenGrafBase
Methode berechnen. Clients, die verwendet wurden,GrafDerived.DrawParallelogram
bevor die Basismethode geändert wurde, erwarten jedoch dasGrafDerived
ursprünglich implementierte Verhalten .In Java gibt es keine Möglichkeit für den Autor
GrafDerived
, diese Klasse mit Clients zu koexistieren, die die neueGrafBase.DrawParallelogram
Methode verwenden (und möglicherweise nicht wissen, dass sieGrafDerived
überhaupt vorhanden ist), ohne die Kompatibilität mit vorhandenem Clientcode zu beeinträchtigen, derGrafDerived.DrawParallelogram
zuvorGrafBase
definiert wurde. Da derDrawParallelogram
Client nicht erkennen kann, welche Art von Client ihn aufruft, muss er sich beim Aufrufen durch verschiedene Client-Codes identisch verhalten. Da die beiden Arten von Client-Code unterschiedliche Erwartungen an ihr Verhalten haben, kann es nichtGrafDerived
vermieden werden, die berechtigten Erwartungen eines der beiden zu verletzen (dh legitimen Client-Code zu brechen).Wenn C #
GrafDerived
nicht erneut kompiliert wird, geht die Laufzeit davon aus, dass Code, der dieDrawParallelogram
Methode bei Verweisen auf einen Typ aufruft ,GrafDerived
das Verhalten erwartet, dasGrafDerived.DrawParallelogram()
bei der letzten Kompilierung vorlag. Code, der die Methode bei Verweisen auf einen Typ aufruftGrafBase
, erwartet jedochGrafBase.DrawParallelogram
das Verhalten, das wurde hinzugefügt). Wird der CompilerGrafDerived
später in Gegenwart des Enhanced neu kompiliertGrafBase
, gibt der Compiler erst dann ein, wenn der Programmierer entweder angibt, ob seine Methode ein gültiger Ersatz für das geerbte Member sein sollGrafBase
, oder ob sein Verhalten an Verweise vom Typ gebunden sein muss, diesGrafDerived
jedoch nicht sollte Ersetzen Sie das Verhalten von Referenzen vom TypGrafBase
.Man könnte vernünftigerweise argumentieren, dass eine
GrafDerived
andere Methode als ein Mitglied mitGrafBase
derselben Signatur auf ein schlechtes Design hindeutet und daher nicht unterstützt werden sollte. Da der Autor eines Basistyps leider nicht weiß, welche Methoden zu abgeleiteten Typen hinzugefügt werden können, und umgekehrt, ist die Situation, in der Basisklassen- und abgeleitete Klassenclients unterschiedliche Erwartungen an gleichnamige Methoden haben, im Wesentlichen unvermeidbar, sofern nicht anders angegeben niemand darf einen Namen hinzufügen, den jemand anderes auch hinzufügen könnte. Die Frage ist nicht, ob eine solche Namensverdoppelung stattfinden soll, sondern wie der Schaden minimiert werden kann, wenn dies der Fall ist.quelle
DerivedGraphics? A class using
GrafDerived?GrafDerived
. Ich habe angefangen zu verwendenDerivedGraphics
, aber ich fand, dass es ein bisschen lang war. EvenGrafDerived
ist immer noch etwas lang, wusste aber nicht, wie man Grafik-Renderer-Typen am besten benennt, die eine klare Beziehung zwischen Basis und Ableitung haben sollten.Standardsituation:
Sie sind Eigentümer einer Basisklasse, die von mehreren Projekten verwendet wird. Sie möchten an der Basisklasse eine Änderung vornehmen, die zwischen 1 und unzähligen abgeleiteten Klassen aufteilt, die in Projekten mit realem Wert vorhanden sind das Ding läuft auf dem Framework). Viel Glück bei den vielbeschäftigten Besitzern der abgeleiteten Klassen; "Nun, Sie müssen sich ändern, Sie sollten diese Methode nicht außer Kraft setzen", ohne den Leuten, die Entscheidungen genehmigen müssen, eine Wiederholung als "Rahmen: Verzögerung von Projekten und Verursacher von Fehlern" zu verschaffen.
Insbesondere, weil Sie nicht als nicht überschreibbar deklariert haben, haben Sie implizit erklärt, dass sie in Ordnung sind, das zu tun, was jetzt Ihre Änderung verhindert.
Und wenn Sie nicht über eine große Anzahl abgeleiteter Klassen verfügen, die durch Überschreiben Ihrer Basisklasse einen realen Wert bieten, warum handelt es sich dann überhaupt um eine Basisklasse? Hoffnung ist ein starker Motivator, aber auch eine sehr gute Möglichkeit, um nicht referenzierten Code zu erhalten.
Endergebnis: Ihr Framework-Basisklassencode wird unglaublich zerbrechlich und statisch, und Sie können die notwendigen Änderungen nicht wirklich vornehmen, um aktuell / effizient zu bleiben. Alternativ erhält Ihr Framework eine Wiederholung der Instabilität (abgeleitete Klassen brechen immer wieder ab), und die Benutzer verwenden es überhaupt nicht, da der Hauptgrund für die Verwendung eines Frameworks darin besteht, die Codierung schneller und zuverlässiger zu machen.
Einfach ausgedrückt, Sie können nicht vielbeschäftigte Projektbesitzer auffordern, ihr Projekt zu verschieben, um die von Ihnen eingeführten Fehler zu beheben, und erwarten, dass alles besser als "weg" ist, es sei denn, Sie bieten ihnen signifikante Vorteile, selbst wenn der ursprüngliche "Fehler" vorliegt. war ihnen, was bestenfalls zu streiten ist.
Es ist besser, sie zunächst nicht das Falsche tun zu lassen. Hier kommt "standardmäßig nicht-virtuell" ins Spiel. Und wenn jemand mit einem sehr klaren Grund zu Ihnen kommt, warum diese bestimmte Methode überschrieben werden muss, und warum es sollte sicher sein, Sie können es "entsperren", ohne zu riskieren, den Code eines anderen zu beschädigen.
quelle
Die Standardeinstellung "Nicht virtuell" setzt voraus, dass der Basisklassenentwickler perfekt ist. Nach meiner Erfahrung sind Entwickler nicht perfekt. Wenn sich der Entwickler einer Basisklasse keinen Anwendungsfall vorstellen kann, bei dem eine Methode überschrieben oder das Hinzufügen einer virtuellen Methode vergessen werden könnte, kann ich den Polymorphismus nicht nutzen, wenn ich die Basisklasse erweitere, ohne die Basisklasse zu ändern. In der realen Welt ist das Ändern der Basisklasse oft keine Option.
In C # vertraut der Basisklassenentwickler dem Unterklassenentwickler nicht. In Java vertraut der Unterklassenentwickler dem Basisklassenentwickler nicht. Der Unterklassenentwickler ist für die Unterklasse verantwortlich und sollte (imho) die Befugnis erhalten, die Basisklasse nach eigenem Ermessen zu erweitern (mit Ausnahme der expliziten Verweigerung, und in Java können sie dies sogar falsch verstehen).
Es ist eine grundlegende Eigenschaft der Sprachdefinition. Es ist nicht richtig oder falsch, es ist was es ist und kann sich nicht ändern.
quelle