Es ist sehr interessant für mich, welche Vorteile ein "global root class" -Ansatz für Framework bietet. In einfachen Worten , was Gründe ergaben , wurde der .NET - Framework entwickelt , um eine Wurzel hat Objektklasse mit dem allgemeinen Funktionalität geeignet für alle Klassen.
Heutzutage entwerfen wir ein neues Framework für den internen Gebrauch (das Framework unter SAP-Plattform) und teilen uns alle in zwei Lager auf - das erste, das der Meinung ist, dass das Framework eine globale Wurzel haben sollte, und das zweite, das der Meinung ist, dass das Gegenteil der Fall ist.
Ich bin im "Global Root" Camp. Und meine Gründe, warum ein solcher Ansatz zu einer guten Flexibilität und Reduzierung der Entwicklungskosten führen würde, sind, dass wir keine allgemeine Funktionalität mehr entwickeln werden.
Daher bin ich sehr daran interessiert, welche Gründe .NET-Architekten wirklich dazu bringen, Framework auf diese Weise zu entwerfen.
quelle
object
im .NET-Framework, zum Teil, weil es einige grundlegende Funktionen für alle Objekte bereitstellt, wie z. B.ToString()
undGetHashCode()
wait()
/notify()
/notifyAll()
und verschmutztclone()
.Antworten:
Die dringlichste Ursache dafür
Object
sind Container (vor Generika), die alles enthalten können, anstatt den C-Stil zu verwenden. Natürlich ist die Vorstellung, dass alles von einer bestimmten Klasse erben und diese Tatsache dann missbrauchen sollte, um jeden Punkt der Typensicherheit zu verlieren, so schrecklich, dass es ein riesiges Warnlicht für den Versand der Sprache ohne Generika gewesen sein sollte, und auch Mittel DasObject
ist für neuen Code völlig überflüssig.quelle
Object
dass nicht geschrieben werden kann umvoid *
in C ++?void*
besser? Ist es nicht. Wenn überhaupt. es ist noch schlimmer .void*
ist in C mit allen impliziten Zeigermodellen schlechter, aber C ++ ist in dieser Hinsicht strenger. Zumindest muss man das explizit besetzen.Ich erinnere mich, dass Eric Lippert einmal sagte, dass das Erben aus der
System.Object
Klasse den besten Wert für den Kunden darstellte. [edit: yep, er hat es hier gesagt ]:Alles, was aus dem
System.Object
Unterricht stammt, bietet eine Zuverlässigkeit und Nützlichkeit, die ich sehr respektiere. Ich weiß, dass alle Objekte einen Typ (GetType
) haben werden, dass sie mit den Finalisierungsmethoden der CLR (Finalize
) übereinstimmen und dass ichGetHashCode
sie beim Umgang mit Sammlungen verwenden kann.quelle
GetType
, Sie können es einfach erweiterntypeof
, um seine Funktionalität zu ersetzen. WasFinalize
dann tatsächlich sind viele Arten gar nicht vollständig finalizable und es eine sinnvolle Methode Finalize nicht besitzen. Darüber hinaus sind viele Typen weder hashbar noch konvertierbar in Zeichenfolgen - insbesondere interne Typen, die niemals für solche Zwecke vorgesehen sind und niemals für den öffentlichen Gebrauch ausgegeben werden. Bei all diesen Typen sind die Schnittstellen unnötig verschmutzt.GetType
,Finalize
oderGetHashCode
für jedenSystem.Object
. Ich habe nur gesagt, dass sie da sind, wenn Sie sie wollen. Bezüglich "ihrer unnötig verschmutzten Schnittstellen" verweise ich auf mein ursprüngliches Zitat von elippert: "bester Wert für den Kunden". Offensichtlich war das .NET-Team bereit, sich um die Kompromisse zu kümmern.Ich denke, dass Java- und C # -Designer ein Root-Objekt hinzugefügt haben, weil es für sie wie beim kostenlosen Mittagessen im Wesentlichen kostenlos war .
Die Kosten für das Hinzufügen eines Stammobjekts entsprechen weitgehend den Kosten für das Hinzufügen Ihrer ersten virtuellen Funktion. Da sowohl CLR als auch JVM Umgebungen mit Garbage Collected und Objekt-Finalizern sind, müssen Sie mindestens eine virtuelle Umgebung für Ihre
java.lang.Object.finalize
oder IhreSystem.Object.Finalize
Methode haben. Daher sind die Kosten für das Hinzufügen Ihres Stammobjekts bereits im Voraus bezahlt. Sie können alle Vorteile nutzen, ohne dafür zu bezahlen. Dies ist das Beste aus beiden Welten: Benutzer, die eine gemeinsame Root-Klasse benötigen, würden das bekommen, was sie wollen, und Benutzer, die es nicht weniger interessieren könnten, könnten so programmieren, als wäre sie nicht vorhanden.quelle
Anscheinend haben Sie hier drei Fragen: Zum einen wurde .NET mit einer gemeinsamen Wurzel versehen, zum anderen mit den Vor- und Nachteilen und zum anderen, ob ein globales Framework-Element eine gute Idee hat Wurzel.
Warum hat .NET eine gemeinsame Wurzel?
In dem am meisten technischen Sinne ist eine gemeinsame Wurzel, die wesentlich für die Reflexion und für Pre-Generika - Container.
Meines Wissens nach hatten sie eine gemeinsame Wurzel mit Basismethoden wie
equals()
undhashCode()
wurden in Java sehr positiv aufgenommen, und C # wurde (unter anderem) von Java beeinflusst, so dass sie auch diese Funktion haben wollten.Was sind die Vor- und Nachteile einer gemeinsamen Wurzel in einer Sprache?
Der Vorteil einer gemeinsamen Wurzel:
equals()
undhashCode()
. Das bedeutet zum Beispiel, dass jedes Objekt in Java oder C # in einer Hash-Map verwendet werden kann - vergleichen Sie diese Situation mit C ++.printf
ähnlichen Methode.object
. Vielleicht nicht sehr verbreitet, aber ohne eine gemeinsame Wurzel wäre das nicht möglich.Der Nachteil einer gemeinsamen Wurzel:
Sollten Sie in Ihrem Framework-Projekt eine gemeinsame Wurzel haben?
Sehr subjektiv natürlich, aber wenn ich mir die obige Pro-Contra-Liste anschaue, würde ich definitiv mit Ja antworten . Insbesondere der letzte Punkt in der Pro-Liste - die Flexibilität, das Verhalten aller Objekte später durch Ändern des Stamms zu ändern - ist in einer Plattform sehr nützlich, in der Änderungen am Stamm mit größerer Wahrscheinlichkeit von Clients akzeptiert werden als Änderungen am gesamten Stamm Sprache.
Außerdem finde ich - obwohl dies noch subjektiver ist - das Konzept einer gemeinsamen Wurzel sehr elegant und ansprechend. Dies erleichtert auch die Verwendung einiger Tools. Beispielsweise können Sie jetzt ein Tool so einstellen, dass es alle Nachkommen dieses gemeinsamen Stamms anzeigt und einen schnellen und guten Überblick über die Framework-Typen erhält.
Natürlich müssen die Typen zumindest ein wenig verwandt sein, und insbesondere würde ich niemals die "Ist-Eine" -Regel opfern, nur um eine gemeinsame Wurzel zu haben.
quelle
Mathematisch gesehen ist es etwas eleganter, ein Typensystem zu haben, das top enthält und das es Ihnen ermöglicht, eine Sprache etwas vollständiger zu definieren.
Wenn Sie ein Framework erstellen, werden die Dinge notwendigerweise von einer vorhandenen Codebasis konsumiert. Sie können keinen universellen Supertyp haben, da alle anderen Typen, die das Framework verwenden, vorhanden sind.
An diesem Punkt hängt die Entscheidung, eine gemeinsame Basisklasse zu haben, davon ab, was Sie tun. Es kommt sehr selten vor, dass viele Dinge ein gemeinsames Verhalten haben und dass es nützlich ist, diese Dinge nur über das gemeinsame Verhalten zu referenzieren.
Aber es kommt vor. Wenn dies der Fall ist, abstrahieren Sie dieses gemeinsame Verhalten direkt.
quelle
Sie haben ein Root-Objekt gesucht, weil sie wollten, dass alle Klassen im Framework bestimmte Dinge unterstützen (einen Hash-Code abrufen, in einen String konvertieren, Gleichheitsprüfungen usw.). Sowohl in C # als auch in Java war es hilfreich, alle diese allgemeinen Funktionen eines Objekts in eine Root-Klasse einzufügen.
Denken Sie daran, dass sie keine OOP-Prinzipien oder Ähnliches verletzen. Alles in der Object-Klasse ist in jeder Unterklasse (sprich: in jeder Klasse) im System sinnvoll . Wenn Sie diesen Entwurf übernehmen möchten, achten Sie darauf, dass Sie diesem Muster folgen. Nehmen Sie also keine Dinge in den Stamm auf, die nicht zu jeder einzelnen Klasse in Ihrem System gehören. Wenn Sie diese Regel sorgfältig befolgen, sehe ich keinen Grund, warum Sie keine Root-Klasse haben sollten, die nützlichen, allgemeinen Code für das gesamte System enthält.
quelle
Eine Reihe von Gründen, die alle untergeordneten Klassen gemeinsam nutzen können. Und es gab den Framework-Autoren auch die Möglichkeit, viele andere Funktionen in das Framework zu schreiben, die alles nutzen konnte. Ein Beispiel wäre die ASP.NET-Zwischenspeicherung und -Sitzungen. In ihnen kann fast alles gespeichert werden, da sie die add-Methoden geschrieben haben, um Objekte zu akzeptieren.
Eine Root-Klasse kann sehr verführerisch sein, ist aber sehr, sehr leicht zu missbrauchen. Wäre es möglich, stattdessen ein Root-Interface zu haben? Und nur eine oder zwei kleine Methoden? Oder würde das viel mehr Code hinzufügen, als für Ihr Framework erforderlich ist?
Ich bin gespannt, welche Funktionen Sie für alle möglichen Klassen in dem Framework benötigen, das Sie schreiben. Wird das wirklich von allen Objekten genutzt? Oder von den meisten? Und wie können Sie verhindern, dass Benutzer nach dem Erstellen der Stammklasse zufällige Funktionen hinzufügen? Oder Zufallsvariablen, die sie "global" sein wollen?
Vor einigen Jahren gab es eine sehr große Anwendung, in der ich eine Stammklasse erstellt habe. Nach einigen Monaten der Entwicklung wurde es mit Code gefüllt, der nichts zu suchen hatte. Wir konvertierten eine alte ASP-Anwendung und die Root-Klasse war der Ersatz für die alte Datei global.inc, die wir in der Vergangenheit verwendet hatten. Musste diese Lektion auf die harte Tour lernen.
quelle
Alle eigenständigen Heap-Objekte erben von
Object
; Dies ist sinnvoll, da alle eigenständigen Heap-Objekte bestimmte gemeinsame Aspekte aufweisen müssen, z. B. die Identifizierung ihres Typs. Wenn der Garbage-Collector einen Verweis auf ein Heap-Objekt eines unbekannten Typs hätte, hätte er keine Möglichkeit zu wissen, welche Bits in dem diesem Objekt zugeordneten Speicherblock als Verweise auf andere Heap-Objekte anzusehen sind.Ferner ist es innerhalb des Typsystems zweckmäßig, den gleichen Mechanismus zum Definieren der Mitglieder von Strukturen und der Mitglieder von Klassen zu verwenden. Das Verhalten von Speicherorten vom Typ "Wert" (Variablen, Parameter, Felder, Array-Slots usw.) unterscheidet sich stark von dem von Speicherorten vom Typ "Klasse". Solche Verhaltensunterschiede treten jedoch bei den Quellcode-Compilern und der Ausführungs-Engine (einschließlich) auf JIT-Compiler), anstatt im Typsystem ausgedrückt zu werden.
Eine Konsequenz davon ist, dass das Definieren eines Werttyps effektiv zwei Typen definiert - einen Speicherorttyp und einen Heap-Objekttyp. Ersteres kann implizit in Letzteres konvertiert werden, und Letzteres kann durch Typumwandlung in Ersteres konvertiert werden. Beide Arten der Konvertierung funktionieren, indem alle öffentlichen und privaten Felder von einer Instanz des betreffenden Typs in eine andere kopiert werden. Darüber hinaus ist es mit generischen Einschränkungen möglich, Schnittstellenelemente an einem Speicherort vom Werttyp direkt aufzurufen, ohne zuvor eine Kopie davon zu erstellen.
All dies ist wichtig, da Verweise auf Heap-Objekte vom Typ Wert sich wie Klassenverweise und nicht wie Werttypen verhalten. Betrachten Sie beispielsweise den folgenden Code:
Wenn der
testEnumerator()
Methode ein Speicherort vom Werttyp übergeben wird,it
wird eine Instanz empfangen, deren öffentliche und private Felder aus dem übergebenen Wert kopiert werden. Die lokale Variable enthältit2
eine andere Instanz, von der alle Felder kopiert wurdenit
. Der AufrufMoveNext
aufit2
keinen Einfluss aufit
.Wenn der obige Code an einen Speicherort des Klassentyps übergeben wird, beziehen sich die übergebenen Werte
it
undit2
alle auf dasselbe Objekt, und wennMoveNext()
einer von ihnen aufgerufen wird, wird dies effektiv für alle von ihnen aufgerufen.Beachten Sie, dass Casting,
List<String>.Enumerator
um esIEnumerator<String>
effektiv von einem Werttyp in einen Klassentyp umzuwandeln. Der Typ des Heap-Objekts ist,List<String>.Enumerator
aber sein Verhalten unterscheidet sich stark vom gleichnamigen Werttyp.quelle
List<T>.Enumerator
das in a gespeichertList<T>.Enumerator
ist, unterscheidet sich erheblich von dem Verhalten von a, das in a gespeichertIEnumerator<T>
ist, indem das Speichern eines Werts des vorherigen Typs an einem Speicherort eines der beiden Typen eine Kopie des Enumeratorstatus erstellt. Beim Speichern eines Wertes des letzteren Typs an einem Speicherort, der ebenfalls vom letzteren Typ ist, wird keine Kopie erstellt.Object
hat nichts mit dieser Tatsache zu tun.System.Int32
auch eine Instanz von istSystem.Object
, aber ein Speicherort des TypsSystem.Int32
weder eineSystem.Object
Instanz noch eine Referenz auf eine enthält.Dieses Design geht wirklich auf Smalltalk zurück, das ich größtenteils als Versuch ansehen würde, die Objektorientierung auf Kosten fast aller anderen Bedenken zu verfolgen. Als solches tendiert es (meiner Meinung nach) dazu, Objektorientierung zu verwenden, selbst wenn andere Techniken wahrscheinlich (oder sogar sicher) überlegen sind.
Wenn Sie eine einzelne Hierarchie mit
Object
(oder etwas Ähnlichem) im Stammverzeichnis haben, ist es (zum Beispiel) ziemlich einfach, Ihre Auflistungsklassen als Auflistungen von zu erstellen. Daher istObject
es für eine Auflistung trivial, Objekte jeder Art zu enthalten.Als Gegenleistung für diesen eher geringen Vorteil erhalten Sie jedoch eine ganze Reihe von Nachteilen. Aus Sicht des Designs haben Sie zunächst einige wirklich verrückte Ideen. Was haben Atheismus und ein Wald nach der Java-Sicht des Universums gemeinsam? Dass beide Hashcodes haben! Ist eine Karte eine Sammlung? Laut Java ist das nicht der Fall!
In den 70er Jahren, als Smalltalk entworfen wurde, wurde diese Art von Unsinn akzeptiert, vor allem, weil niemand eine vernünftige Alternative entworfen hatte. Smalltalk wurde 1980 fertiggestellt und 1983 wurde Ada (einschließlich Generika) entwickelt. Obwohl Ada nie die von manchen vorhergesagte Popularität erlangte, reichten seine Generika aus, um Sammlungen von Objekten beliebigen Typs zu unterstützen - ohne den Wahnsinn, der den monolithischen Hierarchien innewohnt.
Bei der Entwicklung von Java (und in geringerem Maße von .NET) wurde die monolithische Klassenhierarchie wahrscheinlich als "sichere" Wahl angesehen - eine mit Problemen, aber meist bekannten Problemen. Im Gegensatz dazu war die generische Programmierung eine, die fast jeder (schon damals) als eine theoretisch viel bessere Herangehensweise an das Problem erkannte , die jedoch von vielen kommerziell orientierten Entwicklern als wenig erforscht und / oder riskant angesehen wurde (dh in der Geschäftswelt) (Ada wurde weitgehend als Misserfolg abgetan).
Lassen Sie mich jedoch klar und deutlich sein: Die monolithische Hierarchie war ein Fehler. Die Gründe für diesen Fehler waren zumindest verständlich, aber es war trotzdem ein Fehler. Es ist ein schlechtes Design und seine Designprobleme durchdringen fast den gesamten Code, der es verwendet.
Für ein neues Design gibt es heute jedoch keine vernünftige Frage: Die Verwendung einer monolithischen Hierarchie ist ein klarer Fehler und eine schlechte Idee.
quelle
Was Sie tun möchten, ist tatsächlich interessant. Es ist schwer zu beweisen, ob es falsch oder richtig ist, bis Sie fertig sind.
Hier sind einige Dinge zum Nachdenken:
Dies sind die einzigen zwei Dinge, die von einer gemeinsamen Wurzel profitieren können. In einer Sprache werden einige sehr gebräuchliche Methoden definiert, mit denen Sie Objekte übergeben können, die noch nicht definiert wurden, die jedoch für Sie nicht gelten sollten.
Auch aus eigener Erfahrung:
Ich habe ein Toolkit verwendet, das einmal von einem Entwickler mit Smalltalk-Hintergrund geschrieben wurde. Er machte es so, dass alle seine "Data" -Klassen eine einzelne Klasse erweiterten und alle Methoden diese Klasse beanspruchten - soweit so gut. Das Problem ist, dass die verschiedenen Datenklassen nicht immer austauschbar waren. Mein Editor und mein Compiler konnten mir also KEINE Hilfestellung geben, was in eine bestimmte Situation zu übergehen ist, und ich musste auf seine Dokumente verweisen, die nicht immer hilfreich waren. .
Das war die schwierigste Bibliothek, mit der ich jemals umgehen musste.
quelle
Weil das der Weg der OOP ist. Es ist wichtig, einen Typ (Objekt) zu haben, der auf alles verweisen kann. Ein Argument vom Typ Objekt kann alles annehmen. Es gibt auch Vererbung, Sie können sicher sein, dass ToString (), GetHashCode () usw. für alles und jeden verfügbar sind.
Sogar Oracle hat die Bedeutung eines solchen Basistyps erkannt und plant, Primitive in Java ab 2017 zu entfernen
quelle
ToString()
undGetType()
auf jedem Objekt sein werden. Es ist wahrscheinlich erwähnenswert, dass Sie eine Variable vom Typ verwenden könnenobject
, um ein beliebiges .NET-Objekt zu speichern.