Warum wurde C # im Gegensatz zu Java mit den Schlüsselwörtern "new" und "virtual + override" erstellt?

61

In Java gibt es keine virtual, new, overrideSchlü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 newoder virtual+deriveoder neue + virtuelle Überschreibung funktioniert.

Ich kann nicht verstehen, warum in der Welt ich eine Methode DerivedClassmit demselben Namen und derselben Signatur wie hinzufügen BaseClassund ein neues Verhalten definieren werde, aber beim Laufzeit-Polymorphismus wird die BaseClassMethode aufgerufen! (was nicht übergeordnet ist, aber logischerweise sollte es sein).

Im Fall virtual + overridewenn 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 + overrideanstelle von newund auch an die Verwendung newanstelle 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 SpaceShipabgeleitet von Vehicle. Dann möchte ich alles carin ein SpaceShipObjekt ä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 SpaceShipnicht die VehicleKlasse überschreibt accelerateund newdann die Logik meines Codes verwendet, wird gebrochen. Ist das nicht ein Nachteil?

Anirban Nag 'tintinmj'
quelle
77
Sie sind nur daran gewöhnt, wie Java es macht, und haben sich einfach nicht die Zeit genommen, die C # -Schlüsselwörter unter eigenen Bedingungen zu verstehen. Ich arbeite mit C #, verstehe die Begriffe sofort und finde heraus, wie Java das seltsam macht.
Robert Harvey
12
IIRC, C # hat dies getan, um "Klarheit zu fördern". Wenn Sie explizit "new" oder "override" sagen müssen, wird dadurch klar und sofort ersichtlich, was gerade passiert, und Sie müssen sich nicht damit herumärgern, herauszufinden, ob die Methode ein bestimmtes Verhalten in einer Basisklasse überschreibt oder nicht. Ich finde es auch sehr nützlich, angeben zu können, welche Methoden ich als virtuell angeben möchte und welche nicht. (Java macht das mit final; es ist genau umgekehrt).
Robert Harvey
15
„In Java gibt es keine virtuellen, neu, Überschreibung Schlüsselwörter für Methodendefinition.“ Es sei denn es gibt @Override.
Svick
3
@svick Anmerkung und Schlüsselwörter sind nicht dasselbe :)
Anirban Nag 'tintinmj'

Antworten:

86

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 newund overridein 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 VehicleKlasse hatte keine Methode PerformEngineCheck().
  • In meiner CarKlasse 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 newkann ich meine Clients nicht beschädigen, wenn die Funktionalität der Basismethode von der abgeleiteten Methode abweicht. Jeder Code, auf den verwiesen Vehiclewird , wird nicht als Car.PerformEngineCheck()aufgerufen angezeigt, aber Code, auf den verwiesen wurde, Carzeigt 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 virtualSchlüsselwort) als auch auf der abgeleiteten Klasse (über das Schlüsselwort newund 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 newdie 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 SpaceShipdas tatsächlich außer Kraft gesetzt wird Vehicle.accelerate()oder ob es anders ist? Es muss der SpaceShipEntwickler sein. Wenn der SpaceShipEntwickler 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()

Omer Iqbal
quelle
12
Der Leistungspunkt ist mittlerweile komplett überholt. Ein Beweis dafür ist mein Benchmark, der zeigt, dass der Zugriff auf ein Feld über eine nicht endgültige, aber niemals überladene Methode einen einzigen Zyklus erfordert.
Maaartinus
7
Sicher, das könnte der Fall sein. Die Frage war, wann C # sich dazu entschied, warum es zu DIESER Zeit tat, und daher ist diese Antwort gültig. Wenn die Frage ist, ob es noch Sinn macht, ist das eine andere Diskussion, IMHO.
Omer Iqbal
1
Ich stimme dir voll und ganz zu.
Maaartinus
2
IMHO, obwohl es definitiv Verwendungen für nicht virtuelle Funktionen gibt, tritt das Risiko unerwarteter Fallstricke auf, wenn etwas, von dem nicht erwartet wird, dass es eine Basisklassenmethode überschreibt oder eine Schnittstelle implementiert, dies tut.
Supercat
3
@ Nilzor: Daher das Argument in Bezug auf akademische vs. pragmatische. Pragmatisch gesehen liegt die Verantwortung für das Brechen von etwas beim letzten Veränderer. Wenn Sie Ihre Basisklasse ändern und eine vorhandene abgeleitete Klasse davon abhängt, dass sie sich nicht ändert, liegt dies nicht an ihrem Problem ("sie" existieren möglicherweise nicht mehr). So werden Basisklassen an das Verhalten ihrer abgeleiteten Klassen gebunden, selbst wenn diese Klasse niemals virtuell gewesen sein sollte . Und wie Sie sagen, gibt es RhinoMock. Ja, wenn Sie alle Methoden korrekt abschließen, ist alles gleichwertig. Hier zeigen wir auf jedes Java-System, das jemals gebaut wurde.
Deworde
94

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.

Doval
quelle
7
Es ist falsch, eine beliebige Methode außer Kraft setzen zu lassen. Sie sollten nur die Methoden überschreiben können, die der Superklasse-Designer als sicher zum Überschreiben festgelegt hat.
Doval
21
Eric Lippert erläutert dies ausführlich in seinem Beitrag Virtuelle Methoden und Spröde Basisklassen .
Brian
12
Java hat einen finalModifikator. Wo liegt also das Problem?
Sarge Borsch
13
@corsiKa "Objekte sollten offen für Erweiterungen sein" - warum? Wenn der Entwickler der Basisklasse nie über Vererbung nachgedacht hat, ist fast garantiert, dass es subtile Abhängigkeiten und Annahmen im Code gibt, die zu Fehlern führen (erinnerst HashMapdu dich ?). Entweder für die Vererbung entwerfen und den Vertrag klarstellen oder nicht und sicherstellen, dass niemand die Klasse erben kann. @Overridewurde 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.
Voo
9
@jwenting "Meinung" ist ein lustiges Wort; Leute mögen es, Fakten beizufügen, damit sie sie ignorieren können. Aber was auch immer es wert ist, es ist auch die "Meinung" von Joshua Bloch (siehe: Effective Java , Punkt 17 ), die "Meinung" von James Gosling (siehe dieses Interview ) und, wie in der Antwort von Omer Iqbal ausgeführt , auch die "Meinung" von Anders Hejlsberg. (Und obwohl Eric Lippert nie angesprochen hat, sich zu entscheiden oder sich zu entscheiden, stimmt er eindeutig zu, dass Vererbung ebenfalls gefährlich ist.) Also, auf wen bezieht sich das?
Doval
33

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, und private, aber Java nur hat public, protectednichts, und private. 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 newvs. virtual+overridegeht es ungefähr so:

  • Wenn Sie Unterklassen zur Implementierung der Methode zwingen möchten, verwenden Sie abstractund overridein der Unterklasse.
  • Wenn Sie Funktionen bereitstellen möchten, der Unterklasse jedoch erlauben, diese zu ersetzen, verwenden Sie virtualund overridein der Unterklasse.
  • Wenn Sie Funktionen bereitstellen möchten, die Unterklassen niemals überschreiben müssen, verwenden Sie nichts.
    • Wenn Sie dann einen Sonderfall Unterklasse haben , die sich anders verhalten müssen, verwenden Sie newin der Unterklasse.
    • Wenn Sie sicherstellen möchten, dass keine Unterklasse das Verhalten jemals überschreiben kann , verwenden Sie sealedin 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 , OrderProcessordie den größten Teil der Logik hatte, mit bestimmten abstract/ virtualMethoden 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) virtualZur Basismethode hinzufügen und overrideim Kind; oder 2) newdem 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.

Bobson
quelle
7
Ich denke, dass die absichtliche Verwendung newdieses 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 so newverhalten sich ed-Methoden nicht.
Svick
@svick - Möglicherweise ja. In unserem speziellen Szenario würde das niemals vorkommen, aber deshalb habe ich die Einschränkung hinzugefügt.
Bobson
Eine hervorragende Verwendung newist in WebViewPage <TModel> im MVC-Framework. Aber ich bin auch newstundenlang von einem Bug befallen , deshalb halte ich es nicht für eine unvernünftige Frage.
pdr
1
@ C.Champagne: Jedes Werkzeug kann schlecht benutzt werden und dieses ist ein besonders scharfes Werkzeug - Sie können sich leicht schneiden. Dies ist jedoch kein Grund, das Tool aus der Toolbox zu entfernen und eine Option aus einem talentierteren API-Designer zu entfernen.
pdr
1
@svick Richtig, weshalb es normalerweise eine Compiler-Warnung ist. Die Fähigkeit, "neu" zu werden, deckt jedoch Randbedingungen (wie die angegebene) ab und macht sogar deutlich, dass Sie etwas Seltsames tun, wenn Sie den unvermeidlichen Fehler diagnostizieren. "Warum ist diese Klasse und nur diese Klasse Buggy ... ah-hah, eine" neue ", lass uns testen, wo die SuperClass verwendet wird".
Deworde
7

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, GrafBasees wird nicht implementiert void DrawParallelogram(int x1,int y1, int x2,int y2, int x3,int y3), sondern GrafDerivedes wird als öffentliche Methode implementiert , die ein Parallelogramm zeichnet, dessen berechneter vierter Punkt dem ersten entgegengesetzt ist. Angenommen, eine spätere Version von GrafBaseimplementiert eine öffentliche Methode mit derselben Signatur, jedoch mit dem berechneten vierten Punkt gegenüber dem zweiten. Clients, die erwarten, dass a, GrafBaseaber eine Referenz auf a erhalten, GrafDerivedwerden DrawParallelogramden vierten Punkt in der Art und Weise der neuen GrafBaseMethode berechnen. Clients, die verwendet wurden, GrafDerived.DrawParallelogrambevor die Basismethode geändert wurde, erwarten jedoch das GrafDerivedursprünglich implementierte Verhalten .

In Java gibt es keine Möglichkeit für den Autor GrafDerived, diese Klasse mit Clients zu koexistieren, die die neue GrafBase.DrawParallelogramMethode verwenden (und möglicherweise nicht wissen, dass sie GrafDerivedüberhaupt vorhanden ist), ohne die Kompatibilität mit vorhandenem Clientcode zu beeinträchtigen, der GrafDerived.DrawParallelogramzuvor GrafBasedefiniert wurde. Da der DrawParallelogramClient 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 nicht GrafDerivedvermieden werden, die berechtigten Erwartungen eines der beiden zu verletzen (dh legitimen Client-Code zu brechen).

Wenn C # GrafDerivednicht erneut kompiliert wird, geht die Laufzeit davon aus, dass Code, der die DrawParallelogramMethode bei Verweisen auf einen Typ aufruft , GrafDeriveddas Verhalten erwartet, das GrafDerived.DrawParallelogram()bei der letzten Kompilierung vorlag. Code, der die Methode bei Verweisen auf einen Typ aufruft GrafBase, erwartet jedoch GrafBase.DrawParallelogramdas Verhalten, das wurde hinzugefügt). Wird der Compiler GrafDerivedspäter in Gegenwart des Enhanced neu kompiliert GrafBase, gibt der Compiler erst dann ein, wenn der Programmierer entweder angibt, ob seine Methode ein gültiger Ersatz für das geerbte Member sein soll GrafBase, oder ob sein Verhalten an Verweise vom Typ gebunden sein muss, dies GrafDerivedjedoch nicht sollte Ersetzen Sie das Verhalten von Referenzen vom Typ GrafBase.

Man könnte vernünftigerweise argumentieren, dass eine GrafDerivedandere Methode als ein Mitglied mit GrafBasederselben 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.

Superkatze
quelle
2
Ich denke, es gibt hier eine gute Antwort, aber sie geht in der Textwand verloren. Bitte teilen Sie dies in ein paar weitere Absätze auf.
Bobson
Was ist DerivedGraphics? A class using GrafDerived?
C.Champagne
@ C.Champagne: Ich meinte GrafDerived. Ich habe angefangen zu verwenden DerivedGraphics, aber ich fand, dass es ein bisschen lang war. Even GrafDerivedist 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.
Supercat
1
@ Bobson: Besser?
Supercat
6

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.

entwurde
quelle
0

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.

clish
quelle
2
Dies scheint nichts Wesentliches über die in den vorherigen 5 Antworten gemachten und erklärten Punkte zu bieten
Mücke