Warum implementiert C # Methoden standardmäßig als nicht virtuell?

105

Warum behandelt C # Methoden im Gegensatz zu Java standardmäßig als nicht virtuelle Funktionen? Ist es eher ein Leistungsproblem als andere mögliche Ergebnisse?

Ich erinnere mich daran, einen Absatz von Anders Hejlsberg über einige Vorteile der bestehenden Architektur gelesen zu haben. Aber was ist mit Nebenwirkungen? Ist es wirklich ein guter Kompromiss, standardmäßig nicht virtuelle Methoden zu haben?

Burcu Dogan
quelle
1
Die Antworten , die Leistungsgründen erwähnen die Tatsache übersehen , dass der C # Compiler meist Methodenaufrufe kompiliert callvirt und nicht nennen . Aus diesem Grund ist es in C # nicht möglich, eine Methode zu haben, die sich anders verhält, wenn die thisReferenz null ist. Sehen Sie hier für weitere Informationen.
Andy
Wahr! Der Aufruf der IL-Anweisung gilt hauptsächlich für Aufrufe statischer Methoden.
RBT
1
Die Gedanken des C # -Architekten Anders Hejlsberg hier und hier .
RBT

Antworten:

98

Klassen sollten so konzipiert sein, dass die Vererbung sie nutzen kann. virtualStandardmäßig Methoden zu haben bedeutet, dass jede Funktion in der Klasse ausgesteckt und durch eine andere ersetzt werden kann, was nicht wirklich gut ist. Viele Leute glauben sogar, dass Klassen sealedstandardmäßig sein sollten.

virtualMethoden können auch geringfügige Auswirkungen auf die Leistung haben. Dies ist jedoch wahrscheinlich nicht der Hauptgrund.

Mehrdad Afshari
quelle
7
Persönlich bezweifle ich diesen Teil über die Leistung. Selbst für virtuelle Funktionen kann der Compiler sehr gut herausfinden, wo er callvirtdurch einen einfachen callim IL-Code ersetzt werden muss (oder noch weiter stromabwärts beim JITting). Java HotSpot macht dasselbe. Der Rest der Antwort ist genau richtig.
Konrad Rudolph
5
Ich bin damit einverstanden, dass die Leistung nicht im Vordergrund steht, aber sie kann auch Auswirkungen auf die Leistung haben. Es ist wahrscheinlich sehr klein. Dies ist jedoch sicherlich nicht der Grund für diese Wahl. Ich wollte das nur erwähnen.
Mehrdad Afshari
71
Versiegelte Klassen bringen mich dazu, Babys schlagen zu wollen.
mxmissile
6
@mxmissile: Ich auch, aber gutes API-Design ist sowieso schwer. Ich denke, standardmäßig versiegelt macht Sinn für Code, den Sie besitzen. Meistens hat der andere Programmierer in Ihrem Team die Vererbung nicht berücksichtigt, als er die von Ihnen benötigte Klasse implementiert hat, und standardmäßig versiegelt würde diese Nachricht recht gut vermittelt.
Roman Starkov
49
Viele Menschen sind auch Psychopathen, das heißt nicht, dass wir ihnen zuhören sollten. Virtuell sollte die Standardeinstellung in C # sein, Code sollte erweiterbar sein, ohne dass Implementierungen geändert oder vollständig neu geschrieben werden müssen. Menschen, die ihre APIs übermäßig schützen, haben häufig tote oder halb genutzte APIs, was weitaus schlimmer ist als das Szenario, dass jemand sie missbraucht.
Chris Nicola
89

Ich bin überrascht, dass es hier einen solchen Konsens zu geben scheint, dass nicht virtuell standardmäßig der richtige Weg ist, Dinge zu tun. Ich werde auf der anderen - ich denke pragmatischen - Seite des Zauns herunterkommen.

Die meisten Rechtfertigungen lesen sich für mich wie das alte Argument "Wenn wir Ihnen die Macht geben, könnten Sie sich selbst verletzen". Von Programmierern?!

Mir scheint, dass der Codierer, der nicht genug wusste (oder nicht genug Zeit hatte), um seine Bibliothek für Vererbung und / oder Erweiterbarkeit zu entwerfen, der Codierer ist, der genau die Bibliothek erstellt hat, die ich wahrscheinlich reparieren oder optimieren muss - genau die Bibliothek, in der die Fähigkeit zum Überschreiben am nützlichsten wäre.

Die Häufigkeit, mit der ich hässlichen, verzweifelten Workaround-Code schreiben musste (oder die Verwendung aufgeben und meine eigene alternative Lösung verwenden musste), weil ich nicht weit überschreiben kann, überwiegt bei weitem die Häufigkeit, mit der ich jemals gebissen wurde ( zB in Java) durch Überschreiben, wo der Designer es vielleicht nicht in Betracht gezogen hat.

Standardmäßig nicht virtuell macht mein Leben schwerer.

UPDATE: Es wurde [ganz richtig] darauf hingewiesen, dass ich die Frage nicht wirklich beantwortet habe. Also - und mit Entschuldigung, dass ich ziemlich spät bin ...

Ich wollte in der Lage sein, etwas Markiges zu schreiben wie "C # implementiert Methoden standardmäßig als nicht virtuell, weil eine schlechte Entscheidung getroffen wurde, die Programme höher bewertet als Programmierer". (Ich denke, das könnte aufgrund einiger anderer Antworten auf diese Frage gerechtfertigt sein - wie Leistung (vorzeitige Optimierung, irgendjemand?) Oder Gewährleistung des Verhaltens von Klassen.)

Mir ist jedoch klar, dass ich nur meine Meinung äußern würde und nicht die endgültige Antwort, die Stack Overflow wünscht. Sicherlich dachte ich, auf höchster Ebene lautet die endgültige (aber nicht hilfreiche) Antwort:

Sie sind standardmäßig nicht virtuell, da die Sprachdesigner eine Entscheidung treffen mussten und genau das haben sie gewählt.

Jetzt denke ich, den genauen Grund, warum sie diese Entscheidung getroffen haben, werden wir nie ... oh, warte! Das Protokoll eines Gesprächs!

Es scheint also, dass die Antworten und Kommentare hier zu den Gefahren des Überschreibens von APIs und der Notwendigkeit, explizit für die Vererbung zu entwerfen, auf dem richtigen Weg sind, aber alle einen wichtigen zeitlichen Aspekt vermissen: Anders 'Hauptanliegen war es, das Implizite einer Klasse oder API beizubehalten Vertrag über Versionen . Und ich denke, er ist eher besorgt darüber, dass sich die .Net / C # -Plattform unter Code ändern kann, als darüber, dass sich der Benutzercode über der Plattform ändert. (Und sein "pragmatischer" Standpunkt ist genau das Gegenteil von meinem, weil er von der anderen Seite schaut.)

(Aber hätten sie nicht einfach standardmäßig virtuell auswählen und dann "final" durch die Codebasis pfeffern können? Vielleicht ist das nicht ganz dasselbe ... und Anders ist eindeutig schlauer als ich, also werde ich es lügen lassen.)

mwardm
quelle
2
Könnte Ihnen nicht mehr zustimmen. Sehr frustrierend, wenn Sie eine Drittanbieter-API konsumieren und ein bestimmtes Verhalten außer Kraft setzen möchten und dies nicht können.
Andy
3
Ich stimme begeistert zu. Wenn Sie eine API veröffentlichen möchten (intern in einem Unternehmen oder in der Außenwelt), können und sollten Sie wirklich sicherstellen, dass Ihr Code für die Vererbung ausgelegt ist. Sie sollten die API gut und ausgefeilt machen, wenn Sie sie veröffentlichen, damit sie von vielen Menschen verwendet werden kann. Verglichen mit dem Aufwand für ein gutes Gesamtdesign (guter Inhalt, klare Anwendungsfälle, Tests, Dokumentation) ist das Design für die Vererbung wirklich nicht schlecht. Und wenn Sie nicht veröffentlichen, ist die virtuelle Standardeinstellung weniger zeitaufwändig, und Sie können immer den kleinen Teil der Fälle beheben, in denen dies problematisch ist.
Schwerkraft
2
Wenn es in Visual Studio nur eine Editorfunktion gab, mit der alle Methoden / Eigenschaften automatisch als virtual... Visual Studio-Add-In gekennzeichnet werden können?
Kevinarpe
1
Obwohl ich Ihnen voll und ganz zustimme, ist dies nicht gerade eine Antwort.
Chris Morgan
2
Angesichts moderner Entwicklungspraktiken wie Abhängigkeitsinjektion, Verspottungs-Frameworks und ORMs scheint es klar zu sein, dass unsere C # -Designer die Marke ein wenig verfehlt haben. Es ist sehr frustrierend, eine Abhängigkeit testen zu müssen, wenn Sie eine Eigenschaft standardmäßig nicht überschreiben können.
Jeremy Holovacs
17

Weil es zu leicht zu vergessen ist, dass eine Methode möglicherweise überschrieben wird und nicht dafür entworfen wurde. C # bringt Sie zum Nachdenken, bevor Sie es virtuell machen. Ich denke, das ist eine großartige Designentscheidung. Einige Leute (wie Jon Skeet) haben sogar gesagt, dass Klassen standardmäßig versiegelt werden sollten.

Zifre
quelle
12

Um zusammenzufassen, was andere gesagt haben, gibt es einige Gründe:

1- In C # gibt es viele Dinge in Syntax und Semantik, die direkt aus C ++ stammen. Die Tatsache, dass Methoden in C ++ standardmäßig nicht virtuell waren, beeinflusste C #.

2- Standardmäßig ist es ein Leistungsproblem, wenn jede Methode virtuell ist, da bei jedem Methodenaufruf die virtuelle Tabelle des Objekts verwendet werden muss. Darüber hinaus schränkt dies die Fähigkeit des Just-In-Time-Compilers, Inline-Methoden und andere Arten der Optimierung durchzuführen, stark ein.

3- Vor allem, wenn Methoden standardmäßig nicht virtuell sind, können Sie das Verhalten Ihrer Klassen garantieren. Wenn sie standardmäßig virtuell sind, wie in Java, können Sie nicht einmal garantieren, dass eine einfache Getter-Methode wie beabsichtigt funktioniert, da sie überschrieben werden kann, um irgendetwas in einer abgeleiteten Klasse zu tun (natürlich können und sollten Sie das machen Methode und / oder das Klassenfinale).

Man könnte sich fragen, wie Zifre erwähnte, warum die C # -Sprache nicht einen Schritt weiter ging und Klassen standardmäßig versiegelte. Das ist Teil der gesamten Debatte über die Probleme der Vererbung von Implementierungen, was ein sehr interessantes Thema ist.

Trillian
quelle
1
Ich hätte auch Klassen bevorzugt, die standardmäßig versiegelt sind. Dann wäre es konsequent.
Andy
9

C # wird von C ++ (und mehr) beeinflusst. C ++ aktiviert standardmäßig nicht den dynamischen Versand (virtuelle Funktionen). Ein (gutes?) Argument dafür ist die Frage: "Wie oft implementieren Sie Klassen, die Mitglieder einer Klasse hiearchy sind?". Ein weiterer Grund, das dynamische Versenden standardmäßig nicht zu aktivieren, ist der Speicherbedarf. Eine Klasse ohne virtuellen Zeiger (vpointer), der auf eine virtuelle Tabelle zeigt , ist natürlich kleiner als die entsprechende Klasse mit aktivierter später Bindung.

Das Leistungsproblem ist nicht so einfach mit "Ja" oder "Nein" zu beantworten. Der Grund dafür ist die Just In Time (JIT) -Kompilierung, die eine Laufzeitoptimierung in C # darstellt.

Eine andere, ähnliche Frage zur " Geschwindigkeit virtueller Anrufe ".

Schildmeijer
quelle
Ich bin etwas zweifelhaft, dass virtuelle Methoden aufgrund des JIT-Compilers Auswirkungen auf die Leistung von C # haben. Dies ist einer der Bereiche, in denen JIT besser sein kann als die Offline-Kompilierung, da sie Funktionsaufrufe
einbinden können, die
1
Eigentlich denke ich, dass es mehr von Java beeinflusst wird als von C ++, was dies standardmäßig tut.
Mehrdad Afshari
5

Der einfache Grund sind neben den Leistungskosten auch die Konstruktions- und Wartungskosten. Eine virtuelle Methode hat im Vergleich zu einer nicht virtuellen Methode zusätzliche Kosten, da der Designer der Klasse planen muss, was passiert, wenn die Methode von einer anderen Klasse überschrieben wird. Dies hat große Auswirkungen, wenn Sie erwarten, dass eine bestimmte Methode den internen Status aktualisiert oder ein bestimmtes Verhalten aufweist. Sie müssen jetzt planen, was passiert, wenn eine abgeleitete Klasse dieses Verhalten ändert. In dieser Situation ist es viel schwieriger, zuverlässigen Code zu schreiben.

Mit einer nicht virtuellen Methode haben Sie die vollständige Kontrolle. Alles, was schief geht, ist die Schuld des ursprünglichen Autors. Der Code ist viel einfacher zu überlegen.

JaredPar
quelle
Dies ist ein wirklich alter Beitrag, aber für einen Neuling, der sein erstes Projekt schreibt, ärgere ich mich ständig über unbeabsichtigte / unbekannte Konsequenzen des Codes, den ich schreibe. Es ist sehr beruhigend zu wissen, dass nicht virtuelle Methoden völlig MEINE Schuld sind.
Trevorc
2

Wenn alle C # -Methoden virtuell wären, wäre das vtbl viel größer.

C # -Objekte verfügen nur über virtuelle Methoden, wenn für die Klasse virtuelle Methoden definiert sind. Es ist wahr, dass alle Objekte Typinformationen haben, die ein vtbl-Äquivalent enthalten. Wenn jedoch keine virtuellen Methoden definiert sind, sind nur die Basisobjektmethoden vorhanden.

@ Tom Hawtin: Es ist wahrscheinlich genauer zu sagen, dass C ++, C # und Java alle aus der C-Sprachfamilie stammen :)

Thomas Bratt
quelle
1
Warum sollte es ein Problem für die vtable sein, viel größer zu sein? Es gibt nur 1 Tabelle pro Klasse (und nicht pro Instanz), daher macht die Größe keinen großen Unterschied.
Schwerkraft
1

Aus einem Perl-Hintergrund kommend, denke ich, hat C # das Schicksal jedes Entwicklers besiegelt, der das Verhalten einer Basisklasse durch eine nicht virtuelle Methode erweitern und ändern wollte, ohne alle Benutzer der neuen Klasse dazu zu zwingen, sich möglicherweise hinter den Kulissen bewusst zu werden Einzelheiten.

Betrachten Sie die List-Klasse 'Add-Methode. Was ist, wenn ein Entwickler eine von mehreren potenziellen Datenbanken aktualisieren möchte, wenn eine bestimmte Liste hinzugefügt wird? Wenn 'Hinzufügen' standardmäßig virtuell gewesen wäre, könnte der Entwickler eine 'BackedList'-Klasse entwickeln, die die' Add'-Methode überschreibt, ohne dass der gesamte Client-Code weiß, dass es sich um eine 'BackedList' anstelle einer regulären 'List' handelt. Für alle praktischen Zwecke kann die 'BackedList' als eine weitere 'Liste' aus dem Client-Code angesehen werden.

Dies ist aus Sicht einer großen Hauptklasse sinnvoll, die möglicherweise Zugriff auf eine oder mehrere Listenkomponenten bietet, die selbst von einem oder mehreren Schemas in einer Datenbank unterstützt werden. Da C # -Methoden standardmäßig nicht virtuell sind, kann die von der Hauptklasse bereitgestellte Liste keine einfache IEnumerable- oder ICollection- oder sogar List-Instanz sein, sondern muss dem Client stattdessen als 'BackedList' angekündigt werden, um die neue Version sicherzustellen der Operation 'Hinzufügen' wird aufgerufen, um das richtige Schema zu aktualisieren.

Travis
quelle
Zwar erleichtern virtuelle Methoden standardmäßig die Arbeit, aber macht es Sinn? Es gibt viele Dinge, die Sie einsetzen könnten, um das Leben leichter zu machen. Warum nicht einfach öffentliche Felder im Unterricht? Das Verhalten zu ändern ist zu einfach. Meiner Meinung nach sollte alles in der Sprache streng gekapselt, starr und standardmäßig widerstandsfähig gegen Änderungen sein. Ändern Sie es nur bei Bedarf. Ich bin nur ein kleiner Philosoph für Designentscheidungen. Fortsetzung ..
Nawfal
... Um über das aktuelle Thema zu sprechen, sollte das Vererbungsmodell nur verwendet werden, wenn dies der Fall Bist A. Wenn Betwas anderes erforderlich ist, Adann ist es nicht A. Ich glaube, dass das Überschreiben als Fähigkeit in der Sprache selbst ein Designfehler ist. Wenn Sie eine andere AddMethode benötigen , ist Ihre Sammlungsklasse keine List. Der Versuch, es zu sagen, ist eine Fälschung. Der richtige Ansatz ist hier die Komposition (und nicht die Fälschung). Das gesamte Framework basiert zwar auf übergeordneten Funktionen, aber ich mag es einfach nicht.
Nawfal
Ich glaube, ich verstehe Ihren Standpunkt, aber anhand des konkreten Beispiels: 'BackedList' könnte nur die Schnittstelle 'IList' implementieren, und der Client kennt nur die Schnittstelle. richtig? vermisse ich etwas Ich verstehe jedoch den weiteren Punkt, den Sie ansprechen möchten.
Vetras
0

Es ist sicherlich kein Leistungsproblem. Der Java-Interpreter von Sun verwendet denselben Code für den Versand ( invokevirtualBytecode), und HotSpot generiert unabhängig davon denselben Code final. Ich glaube, dass alle C # -Objekte (aber keine Strukturen) virtuelle Methoden haben, daher benötigen Sie immer die vtblIdentifikation / runtime class. C # ist ein Dialekt von "Java-ähnlichen Sprachen". Zu behaupten, dass es aus C ++ stammt, ist nicht ganz ehrlich.

Es gibt eine Idee, dass Sie "für die Vererbung entwerfen oder sie verbieten" sollten. Das klingt nach einer großartigen Idee, bis Sie einen schwerwiegenden Geschäftsfall haben, den Sie schnell beheben können. Vielleicht erben Sie von Code, den Sie nicht kontrollieren.

Tom Hawtin - Tackline
quelle
Hotspot ist gezwungen, große Anstrengungen zu unternehmen, um diese Optimierung durchzuführen, gerade weil alle Methoden standardmäßig virtuell sind, was enorme Auswirkungen auf die Leistung hat. CoreCLR ist in der Lage, eine ähnliche Leistung zu erzielen, während es viel einfacher ist
Yair Halberstadt
@YairHalberstadt Hotspot muss aus verschiedenen anderen Gründen in der Lage sein, kompilierten Code zurückzusetzen. Es ist Jahre her, seit ich mir die Quelle angesehen habe, aber der Unterschied zwischen finalund effektiv finalMethoden ist trivial. Es ist auch erwähnenswert, dass es bimorphes Inlining ausführen kann, dh Inline-Methoden mit zwei verschiedenen Implementierungen.
Tom Hawtin - Tackline