Ein junger Mitarbeiter, der OO studierte, hat mich gefragt, warum jedes Objekt als Referenz übergeben wird, was das Gegenteil von primitiven Typen oder Strukturen ist. Es ist ein gemeinsames Merkmal von Sprachen wie Java und C #.
Ich konnte keine gute Antwort für ihn finden.
Was sind die Beweggründe für diese Designentscheidung? Hatten die Entwickler dieser Sprachen es satt, jedes Mal Zeiger und Typedefs erstellen zu müssen?
programming-languages
object-oriented
Gustavo Cardoso
quelle
quelle
Antworten:
Die Hauptgründe dafür sind:
Daher Referenzen.
quelle
Einfache Antwort:
Minimierung des Speicherverbrauchs
und der
CPU-Zeit bei der Neuerstellung und Erstellung einer vollständigen Kopie jedes Objekts, das irgendwo übergeben wurde.
quelle
In C ++ haben Sie zwei Hauptoptionen: Rückgabe nach Wert oder Rückgabe nach Zeiger. Schauen wir uns den ersten an:
Angenommen, Ihr Compiler ist nicht schlau genug, um die Rückgabewertoptimierung zu verwenden, dann geschieht Folgendes:
Wir haben das Objekt sinnlos kopiert. Dies ist eine Verschwendung von Verarbeitungszeit.
Schauen wir uns stattdessen return-by-pointer an:
Wir haben die überflüssige Kopie entfernt, aber jetzt haben wir ein weiteres Problem eingeführt: Wir haben ein Objekt auf dem Heap erstellt, das nicht automatisch zerstört wird. Wir müssen uns selbst darum kümmern:
Zu wissen, wer für das Löschen eines auf diese Weise zugewiesenen Objekts verantwortlich ist, kann nur durch Kommentare oder Konventionen mitgeteilt werden. Es kommt leicht zu Speicherlecks.
Es wurden viele Umgehungsmöglichkeiten vorgeschlagen, um diese beiden Probleme zu lösen - die Rückgabewertoptimierung (bei der der Compiler klug genug ist, die redundante Kopie nicht als Rückgabe nach Wert zu erstellen), indem ein Verweis auf die Methode übergeben wird (die Funktion injiziert also in eine vorhandenes Objekt, anstatt ein neues zu erstellen), intelligente Zeiger (so dass die Frage des Eigentums strittig ist).
Die Java / C # -Ersteller erkannten, dass es eine bessere Lösung ist, Objekte immer als Referenz zurückzugeben, insbesondere, wenn die Sprache dies von Haus aus unterstützt. Es knüpft an viele andere Funktionen der Sprachen an, wie z. B. die Speicherbereinigung usw.
quelle
Viele andere Antworten haben gute Infos. Ich möchte einen wichtigen Punkt zum Klonen hinzufügen , der nur teilweise angesprochen wurde.
Referenzen zu verwenden ist klug. Das Kopieren von Dingen ist gefährlich.
Wie andere gesagt haben, gibt es in Java keinen natürlichen "Klon". Dies ist nicht nur eine fehlende Funktion. Sie möchten niemals einfach jede Eigenschaft in einem Objekt kopieren (egal ob flach oder tief). Was wäre, wenn diese Eigenschaft eine Datenbankverbindung wäre? Sie können eine Datenbankverbindung nicht mehr "klonen", als Sie einen Menschen klonen können. Die Initialisierung ist aus einem bestimmten Grund vorhanden.
Tiefe Kopien sind ein eigenes Problem - wie tief gehen Sie wirklich ? Sie konnten definitiv nichts kopieren, was statisch ist (einschließlich aller
Class
Objekte).Aus dem gleichen Grund, warum es keinen natürlichen Klon gibt, würden Objekte, die als Kopien übergeben werden , Wahnsinn erzeugen . Auch wenn Sie eine DB-Verbindung "klonen" könnten - wie würden Sie jetzt sicherstellen, dass sie geschlossen ist?
* Siehe die Kommentare - Mit dieser "never" -Anweisung meine ich einen automatischen Klon , der jede Eigenschaft klont. Java hat keine bereitgestellt, und aus den hier aufgeführten Gründen ist es für Sie als Benutzer der Sprache wahrscheinlich keine gute Idee, eine eigene zu erstellen. Das Klonen nur von nicht transienten Feldern wäre ein Anfang, aber selbst dann müssten Sie sorgfältig definieren,
transient
wo dies angebracht ist.quelle
clone
ist in Ordnung. Mit "wohl oder übel" meine ich das Kopieren aller Eigenschaften, ohne darüber nachzudenken - ohne absichtliche Absicht. Die Java-Sprachdesigner erzwangen diese Absicht, indem sie eine vom Benutzer erstellte Implementierung von fordertenclone
.Objekte werden in Java immer referenziert. Sie werden nie um sich herum weitergegeben.
Ein Vorteil ist, dass dies die Sprache vereinfacht. Ein C ++ - Objekt kann als Wert oder als Referenz dargestellt werden, sodass für den Zugriff auf ein Mitglied zwei verschiedene Operatoren verwendet werden müssen:
.
und->
. (Es gibt Gründe, warum dies nicht konsolidiert werden kann. Beispielsweise sind intelligente Zeiger Werte, die Referenzen sind und diese voneinander unterscheiden müssen.) Java benötigt nur.
.Ein weiterer Grund ist, dass der Polymorphismus nicht nach Wert, sondern nach Referenz erfolgen muss. Ein durch value behandeltes Objekt ist nur dort und hat einen festen Typ. Es ist möglich, dies in C ++ zu vermasseln.
Außerdem kann Java die Standardzuweisung / Kopie / was auch immer ändern. In C ++ ist es eine mehr oder weniger tiefe Kopie, während es in Java eine einfache Zeigerzuweisung / copy / whatever ist, mit
.clone()
und für den Fall, dass Sie kopieren müssen.quelle
const
, sein Wert kann geändert werden, um auf andere Objekte zu verweisen. Eine Referenz ist ein anderer Name für ein Objekt, kann nicht NULL sein und kann nicht neu platziert werden. Es wird im Allgemeinen durch einfache Verwendung von Zeigern implementiert, aber das ist ein Implementierungsdetail.Ihre ursprüngliche Aussage zu C # -Objekten, die als Referenz übergeben werden, ist nicht korrekt. In C # sind Objekte Referenztypen, aber standardmäßig werden sie wie Werttypen als Wert übergeben. Im Fall eines Referenztyps ist der "Wert", der als Methodenparameter für die Übergabe von Werten kopiert wird, die Referenz selbst, sodass Änderungen an den Eigenschaften innerhalb einer Methode außerhalb des Methodenbereichs wirksam werden.
Wenn Sie jedoch die Parametervariable selbst innerhalb einer Methode neu zuweisen, werden Sie feststellen, dass diese Änderung nicht außerhalb des Methodenbereichs wirksam wird. Wenn Sie dagegen einen Parameter mithilfe des
ref
Schlüsselworts als Referenz übergeben , funktioniert dieses Verhalten wie erwartet.quelle
Schnelle Antwort
Die Designer von Java und anderen Sprachen wollten das Konzept "Alles ist ein Objekt" anwenden. Das Übergeben von Daten als Referenz ist sehr schnell und beansprucht nicht viel Speicher.
Zusätzlicher erweiterter langweiliger Kommentar
Obwohl diese Sprachen Objektreferenzen verwenden (Java, Delphi, C #, VB.NET, Vala, Scala, PHP), ist die Wahrheit, dass Objektreferenzen Zeiger auf Objekte in Verkleidung sind. Der Nullwert, die Speicherzuordnung, die Kopie einer Referenz, ohne die gesamten Daten eines Objekts zu kopieren, allesamt Objektzeiger, keine reinen Objekte !!!
In Object Pascal (nicht Delphi), anc C ++ (nicht Java, nicht C #) kann ein Objekt als statisch zugewiesene Variable deklariert werden, und auch mit einer dynamisch zugewiesenen Variablen, indem ein Zeiger ("Objektreferenz" ohne " Zuckersyntax "). Jeder Fall verwendet eine bestimmte Syntax, und es gibt keine Möglichkeit, wie in Java "und Freunde" zu verwechseln. In diesen Sprachen kann ein Objekt sowohl als Wert als auch als Referenz übergeben werden.
Der Programmierer weiß, wann eine Zeigersyntax erforderlich ist und wann nicht, aber in Java und ähnlichen Sprachen ist dies verwirrend.
Bevor Java existierte oder Mainstream wurde, lernten viele Programmierer OO in C ++ ohne Zeiger, wobei sie bei Bedarf nach Wert oder Referenz übergaben. Beim Wechsel von Lern- zu Geschäftsanwendungen werden normalerweise Objektzeiger verwendet. Die QT-Bibliothek ist dafür ein gutes Beispiel.
Als ich Java lernte, versuchte ich, dem Konzept eines Objekts zu folgen, war aber beim Codieren verwirrt. Schließlich sagte ich "ok, dies sind Objekte, die dynamisch mit einem Zeiger mit der Syntax eines statisch zugewiesenen Objekts zugewiesen wurden" und hatte erneut keine Probleme beim Codieren.
quelle
Java und C # übernehmen die Kontrolle über den Low-Level-Speicher von Ihnen. Der "Haufen", auf dem sich die von Ihnen erstellten Objekte befinden, lebt sein eigenes Leben. Beispielsweise kann der Garbage Collector Objekte nach Belieben abrufen.
Da es eine separate Indirektionsebene zwischen Ihrem Programm und diesem "Heap" gibt, sind die beiden Möglichkeiten, auf ein Objekt nach Wert und Zeiger (wie in C ++) zu verweisen, nicht mehr zu unterscheiden verweisen : Sie verweisen immer auf Objekte, auf die "per Zeiger" verwiesen wird irgendwo auf dem Haufen. Aus diesem Grund wird Pass-by-Reference bei einem solchen Entwurfsansatz zur Standardsemantik der Zuweisung. Java, C #, Ruby und so weiter.
Das Obige betrifft nur imperative Sprachen. In den oben erwähnten Sprachen wird die Kontrolle über den Speicher an die Laufzeit übergeben, aber das Sprachdesign sagt auch "Hey, aber tatsächlich gibt es den Speicher und es gibt die Objekte und sie tun das Gedächtnis besetzen“. Funktionale Sprachen werden noch abstrakter, indem der Begriff "Erinnerung" von ihrer Definition ausgeschlossen wird. Aus diesem Grund gilt die Referenzübergabe nicht unbedingt für alle Sprachen, in denen Sie den Low-Level-Speicher nicht steuern können.
quelle
Ich kann mir ein paar Gründe vorstellen:
Das Kopieren primitiver Typen ist trivial und wird normalerweise in eine Maschinenanweisung übersetzt.
Das Kopieren von Objekten ist nicht trivial. Das Objekt kann Elemente enthalten, die selbst Objekte sind. Das Kopieren von Objekten kostet CPU-Zeit und Speicher. Abhängig vom Kontext gibt es sogar mehrere Möglichkeiten, ein Objekt zu kopieren.
Das Übergeben von Objekten als Referenz ist kostengünstig und wird auch dann nützlich, wenn Sie die Objektinformationen zwischen mehreren Clients des Objekts teilen / aktualisieren möchten.
Komplexe Datenstrukturen (insbesondere solche, die rekursiv sind) erfordern Zeiger. Das Übergeben von Objekten als Referenz ist nur eine sicherere Methode zum Übergeben von Zeigern.
quelle
Andernfalls sollte die Funktion in der Lage sein, automatisch eine (offensichtlich tiefe) Kopie aller Arten von Objekten zu erstellen, die an sie übergeben werden. Und normalerweise kann es nicht erraten, um es zu machen. Sie müssten also die Implementierung der Kopier- / Klon-Methode für alle Ihre Objekte / Klassen definieren.
quelle
Weil Java als besseres C ++ und C # als besseres Java entworfen wurde und die Entwickler dieser Sprachen das grundlegend kaputte C ++ - Objektmodell satt hatten, bei dem Objekte Werttypen sind.
Zwei der drei Grundprinzipien der objektorientierten Programmierung sind Vererbung und Polymorphismus, und die Behandlung von Objekten als Werttypen anstelle von Referenztypen ist mit beiden verheerend. Wenn Sie ein Objekt als Parameter an eine Funktion übergeben, muss der Compiler wissen, wie viele Bytes übergeben werden sollen. Wenn es sich bei Ihrem Objekt um einen Referenztyp handelt, ist die Antwort einfach: Die Größe eines Zeigers, die für alle Objekte gleich ist. Wenn es sich bei Ihrem Objekt jedoch um einen Werttyp handelt, muss er die tatsächliche Größe des Werts übergeben. Da eine abgeleitete Klasse neue Felder hinzufügen kann, bedeutet dies sizeof (abgeleitet)! = Sizeof (Basis), und der Polymorphismus verschwindet aus dem Fenster.
Hier ist ein einfaches C ++ - Programm, das das Problem demonstriert:
Die Ausgabe dieses Programms entspricht nicht der eines entsprechenden Programms in einer vernünftigen OO-Sprache, da Sie ein abgeleitetes Objekt nicht als Wert an eine Funktion übergeben können, die ein Basisobjekt erwartet. Der Compiler erstellt daher einen Konstruktor für verborgene Kopien und übergibt diesen eine Kopie des übergeordneten Teils des untergeordneten Objekts , anstatt das untergeordnete Objekt wie von Ihnen angegeben zu übergeben. Versteckte semantische Fallstricke wie dieser sind der Grund, warum das Übergeben von Objekten als Wert in C ++ vermieden werden sollte und in fast jeder anderen OO-Sprache überhaupt nicht möglich ist.
quelle
Denn sonst gäbe es keinen Polymorphismus.
In der OO-Programmierung können Sie eine größere
Derived
Klasse aus einer erstellenBase
und sie dann an Funktionen übergeben, die a erwartenBase
Eins . Ziemlich trivial, oder?Mit der Ausnahme, dass die Größe des Arguments einer Funktion feststeht und zur Kompilierungszeit festgelegt wird. Sie können alles darüber streiten, was Sie wollen, ausführbarer Code ist wie folgt, und Sprachen müssen an der einen oder anderen Stelle ausgeführt werden (rein interpretierte Sprachen sind hierdurch nicht eingeschränkt ...)
Nun gibt es ein Datenelement, das auf einem Computer genau definiert ist: die Adresse einer Speicherzelle, die normalerweise als ein oder zwei "Wörter" ausgedrückt wird. Es ist entweder als Zeiger oder Referenzen in Programmiersprachen sichtbar.
Um also Objekte beliebiger Länge zu übergeben, ist es am einfachsten, einen Zeiger / Verweis auf dieses Objekt zu übergeben.
Dies ist eine technische Einschränkung der OO-Programmierung.
Aber da Sie bei großen Typen es im Allgemeinen vorziehen, Referenzen zu übergeben, um das Kopieren zu vermeiden, wird dies im Allgemeinen nicht als schwerwiegender Schlag angesehen :)
Es gibt jedoch eine wichtige Konsequenz: Wenn Sie in Java oder C # ein Objekt an eine Methode übergeben, wissen Sie nicht, ob Ihr Objekt von der Methode geändert wird oder nicht. Es erschwert das Debuggen / Parallelisieren, und dies ist das Problem, das Functional Languages und die Transparential Referency zu lösen versuchen -> Kopieren ist nicht so schlimm (wenn es Sinn macht).
quelle
Die Antwort ist im Namen (na ja, fast sowieso). Eine Referenz (wie eine Adresse) verweist nur auf etwas anderes, ein Wert ist eine weitere Kopie von etwas anderem. Ich bin sicher, jemand hat wahrscheinlich etwas zu dem folgenden Effekt erwähnt, aber es wird Umstände geben, in denen einer und nicht der andere geeignet ist (Speichersicherheit vs. Speichereffizienz). Alles dreht sich um die Verwaltung von Speicher, Gedächtnis, Gedächtnis ...... SPEICHER! : D
quelle
Okay, ich sage nicht, dass dies genau der Grund ist, warum Objekte Referenztypen sind oder als Referenz übergeben werden, aber ich kann Ihnen ein Beispiel geben, warum dies auf lange Sicht eine sehr gute Idee ist.
Wenn Sie eine Klasse in C ++ erben, werden alle Methoden und Eigenschaften dieser Klasse physisch in die untergeordnete Klasse kopiert. Es wäre, als würde man den Inhalt dieser Klasse wieder in die Kinderklasse schreiben.
Dies bedeutet, dass die Gesamtgröße der Daten in Ihrer untergeordneten Klasse eine Kombination aus dem Inhalt der übergeordneten Klasse und der abgeleiteten Klasse ist.
EG: #include
Welches würde Ihnen zeigen:
Dies bedeutet, dass bei einer großen Hierarchie mehrerer Klassen die hier angegebene Gesamtgröße des Objekts die Kombination aller dieser Klassen ist. Offensichtlich wären diese Objekte in vielen Fällen beträchtlich groß.
Ich glaube, die Lösung besteht darin, sie auf dem Haufen zu erstellen und Zeiger zu verwenden. Dies bedeutet, dass die Größe von Objekten in Klassen mit mehreren Elternteilen in gewissem Sinne überschaubar wäre.
Aus diesem Grund ist die Verwendung von Referenzen eine bessere Methode, um dies zu tun.
quelle