Alles, was Sie wissen müssen, anhand von Referenzen oder Werten

In der Softwareentwicklung gibt es einige missverstandene Konzepte und missbräuchliche Begriffe. Nach Referenz vs nach Wert ist definitiv einer von ihnen.

Ich erinnere mich an den Tag, als ich mich mit dem Thema befasste und jede Quelle, die ich durchging, der vorherigen zu widersprechen schien. Es dauerte einige Zeit, bis ich es richtig verstanden hatte. Ich hatte keine Wahl, da es ein grundlegendes Thema ist, wenn Sie ein Software-Ingenieur sind.

Ich bin vor ein paar Wochen auf einen bösen Käfer gestoßen und habe mich dazu entschlossen, einen Artikel zu schreiben, damit andere Leute es leichter haben, das Ganze herauszufinden.

Ich programmiere täglich in Ruby. Ich benutze auch ziemlich oft JavaScript, deshalb habe ich diese beiden Sprachen für diese Präsentation ausgewählt.

Um alle Konzepte zu verstehen, werden wir auch einige Go- und Perl-Beispiele verwenden.

Um das ganze Thema zu verstehen, muss man 3 verschiedene Dinge verstehen:

  • Wie die zugrunde liegenden Datenstrukturen in der Sprache implementiert werden (Objekte, Grundtypen, Veränderlichkeit,).
  • So funktioniert die variable Zuordnung / das Kopieren / die Neuzuordnung / der Vergleich
  • Wie Variablen an Funktionen übergeben werden

Zugrunde liegenden Datentypen

In Ruby gibt es keine primitiven Typen und alles ist ein Objekt, einschließlich Ganzzahlen und Booleschen Werten.

Und ja, es gibt eine TrueClass in Ruby.

true.is_a? (TrueClass) => true
3. is_a? (Integer) => true
true.is_a? (Object) => true
3. is_a? (Object) => true
TrueClass.is_a? (Object) => true
Integer.is_a? (Object) => true

Diese Objekte können entweder veränderlich oder unveränderlich sein.

Unveränderlich bedeutet, dass Sie das Objekt nach seiner Erstellung nicht mehr ändern können. Es gibt nur eine Instanz für einen bestimmten Wert mit einer object_id und diese bleibt gleich, egal was Sie tun.

Standardmäßig sind in Ruby die unveränderlichen Objekttypen: Boolean, Numerisch, Null und Symbol.

In der MRT entspricht die object_id eines Objekts dem VALUE, der das Objekt auf der C-Ebene darstellt. Für die meisten Arten von Objekten ist dieser WERT ein Zeiger auf eine Stelle im Speicher, an der die tatsächlichen Objektdaten gespeichert sind.

Von nun an werden wir object_id und memory address austauschbar verwenden.

Führen Sie im MRI einen Ruby-Code für ein unveränderliches Symbol und eine veränderbare Zeichenfolge aus:

: symbol.object_id => 808668
: symbol.object_id => 808668
'string'.object_id => 70137215233780
'string'.object_id => 70137215215120

Während die Symbolversion dieselbe object_id für denselben Wert beibehält, gehören die Zeichenfolgenwerte zu verschiedenen Speicheradressen.

Im Gegensatz zu Ruby hat JavaScript primitive Typen.

Sie sind - Boolean, null, undefined, String und Number.

Die restlichen Datentypen befinden sich unter dem Dach von Objekten (Array, Funktion und Objekt). Es gibt nichts Besonderes hier, es ist viel unkomplizierter als Ruby.

[] instanceof Array => true
[] instanceof Object => true
3 instanceof Object => false

Variable Zuordnung, Kopieren, Neuzuordnung und Vergleich

In Ruby ist jede Variable nur eine Referenz auf ein Objekt (da alles ein Objekt ist).

a = 'string'
b = a
# Wenn Sie a mit demselben Wert neu zuweisen
a = 'string'
setzt b => 'string'
setzt a == b => true # Werte sind gleich
Setzt a.object_id == b.object_id => false # memory adr-s. sich unterscheiden
# Wenn Sie a mit einem anderen Wert neu zuweisen
a = 'neuer String'
setzt a => 'new string'
setzt b => 'string'
setzt a == b => false # Werte sind unterschiedlich
Setzt a.object_id == b.object_id => false # memory adr-s. unterscheiden sich auch

Wenn Sie eine Variable zuweisen, ist dies eine Referenz auf ein Objekt, nicht auf das Objekt selbst. Wenn Sie ein Objekt kopieren, zeigen b = a beide Variablen auf dieselbe Adresse.

Dieses Verhalten wird als Kopieren nach Referenzwert bezeichnet.

Genau genommen wird in Ruby und JavaScript alles nach Wert kopiert.

Bei Objekten handelt es sich jedoch zufällig um die Speicheradressen dieser Objekte. Dank dessen können wir Werte ändern, die sich in diesen Speicheradressen befinden. Dies wird wiederum als Kopie nach Referenzwert bezeichnet, aber die meisten Leute bezeichnen dies als Kopie nach Referenz.

Es wäre eine Referenzkopie, wenn b nach der Neuzuweisung von a zu „neuer String“ auch auf dieselbe Adresse verweisen würde und denselben Wert für „neuer String“ hätte.

Wenn Sie b = a deklarieren, verweisen a und b auf dieselbe SpeicheradresseNach der Neuzuweisung von a (a =

Das selbe mit einem unveränderlichen Typ wie Integer:

a = 1
b = a
a = 1
setzt b => 1
setzt a == b => true # Vergleich nach Wert
Setzt a.object_id == b.object_id => true # Vergleich nach Speicher adr.

Wenn Sie eine der gleichen Ganzzahl neu zuweisen, bleibt die Speicheradresse gleich, da eine bestimmte Ganzzahl immer dieselbe Objekt-ID hat.

Wenn Sie ein Objekt mit einem anderen vergleichen, wird es anhand des Werts verglichen. Wenn Sie überprüfen möchten, ob es sich um dasselbe Objekt handelt, müssen Sie object_id verwenden.

Sehen wir uns die JavaScript-Version an:

var a = 'string';
var b = a;
a = 'string'; # a wird dem gleichen Wert zugewiesen
console.log (a); => 'string'
console.log (b); => 'string'
console.log (a === b); => true // Vergleich nach Wert
var a = [];
var b = a;
console.log (a === b); => wahr
a = [];
console.log (a); => []
console.log (b); => []
console.log (a === b); => false // Vergleich nach Speicheradresse

Ausgenommen der Vergleich - JavaScript wird nach Wert für primitive Typen und nach Referenz für Objekte verwendet. Das Verhalten sieht genauso aus wie in Ruby.

Nicht ganz.

Primitive Werte in JavaScript werden nicht von mehreren Variablen gemeinsam genutzt. Auch wenn Sie die Variablen gleich setzen. Jede Variable, die einen Grundwert darstellt, gehört garantiert zu einem eindeutigen Speicherort.

Dies bedeutet, dass keine der Variablen jemals auf dieselbe Speicheradresse verweist. Es ist auch wichtig, dass der Wert selbst an einem physischen Speicherort gespeichert wird.

In unserem Beispiel zeigt b, wenn wir b = a deklarieren, sofort auf eine andere Speicheradresse mit demselben 'String'-Wert. Sie müssen a also nicht neu zuweisen, um auf eine andere Speicheradresse zu verweisen.

Dies wird als vom Wert kopiert bezeichnet, da Sie nur auf den Wert keinen Zugriff auf die Speicheradresse haben.

Wenn Sie a = b deklarieren, wird es durch den Wert zugewiesen, sodass a und b auf unterschiedliche Speicheradressen verweisen

Sehen wir uns ein besseres Beispiel an, bei dem es darauf ankommt.

Wenn wir in Ruby den Wert in der Speicheradresse ändern, haben alle Verweise, die auf die Adresse verweisen, denselben aktualisierten Wert:

a = 'x'
b = a
a.concat ('y')
setzt a => 'xy'
setzt b => 'xy'
b.concat ('z')
setzt a => 'xyz'
setzt b => 'xyz'
a = 'z'
setzt a => 'z'
setzt b => 'xyz'
a [0] = 'y'
setzt a => 'y'
setzt b => 'xyz'

Sie könnten denken, in JavaScript würde sich nur der Wert von a ändern, aber nein. Sie können den ursprünglichen Wert nicht einmal ändern, da Sie keinen direkten Zugriff auf die Speicheradresse haben.

Sie könnten sagen, Sie haben a ein "x" zugewiesen, aber es wurde nach Wert zugewiesen, sodass die Speicheradresse von a den Wert "x" enthält. Sie können ihn jedoch nicht ändern, da Sie keinen Verweis darauf haben.

var a = 'x';
var b = a;
a.concat ('y');
console.log (a); => 'x'
console.log (b); => 'x'
a [0] = 'z';
console.log (a); => 'x';

Das Verhalten von JavaScript-Objekten und die Implementierung sind mit Rubys veränderbaren Objekten identisch. Beide Kopien sind Referenzwerte.

JavaScript-Primitivtypen werden nach Wert kopiert. Das Verhalten ist dasselbe wie bei Rubys unveränderlichen Objekten, die nach Referenzwert kopiert werden.

Huh?

Wenn Sie etwas nach Wert kopieren, bedeutet dies wiederum, dass Sie den ursprünglichen Wert nicht ändern (mutieren) können, da kein Verweis auf die Speicheradresse vorhanden ist. Aus der Sicht des schreibenden Codes entspricht dies unveränderlichen Entitäten, die Sie nicht mutieren können.

Wenn Sie Ruby und JavaScript vergleichen, ist der einzige Datentyp, der sich standardmäßig anders verhält, "String" (aus diesem Grund haben wir in den obigen Beispielen "String" verwendet).

In Ruby ist es ein veränderbares Objekt und wird als Referenzwert kopiert / übergeben, während es in JavaScript ein primitiver Typ ist und als Wert kopiert / übergeben wird.

Wenn Sie ein Objekt klonen (nicht kopieren) möchten, müssen Sie dies explizit in beiden Sprachen tun, damit Sie sicherstellen können, dass das ursprüngliche Objekt nicht geändert wird:

a = {'name': 'Kate'}
b = a.clone
b ['name'] = 'Anna'
setzt ein => {: name => "Kate"}
var a = {'name': 'Kate'};
var b = {... a}; // mit der neuen ES6-Syntax
b ['name'] = 'Anna';
console.log (a); => {name: "Kate"}

Beachten Sie dies unbedingt, da Sie sonst auf einige üble Fehler stoßen, wenn Sie Ihren Code mehr als einmal aufrufen. Ein gutes Beispiel wäre eine rekursive Funktion, bei der Sie das Objekt als Argument verwenden.

Ein anderes ist React (JavaScript-Front-End-Framework), bei dem Sie immer ein neues Objekt übergeben müssen, um den Status zu aktualisieren, da der Vergleich anhand der Objekt-ID erfolgt.

Dies ist schneller, da Sie das Objekt nicht Zeile für Zeile durchgehen müssen, um zu prüfen, ob es geändert wurde.

Wie Variablen an Funktionen übergeben werden

Das Übergeben von Variablen an Funktionen funktioniert in den meisten Sprachen genauso wie das Kopieren für dieselben Datentypen.

In JavaScript werden primitive Typen kopiert und als Wert übergeben, und Objekte werden kopiert und als Referenzwert übergeben.

Ich denke, dies ist der Grund, warum die Leute nur über Wertübergabe oder Referenzübergabe sprechen und anscheinend nie von Kopieren sprechen. Ich vermute, sie gehen davon aus, dass das Kopieren genauso funktioniert.

a = 'b'
def output (string) # übergeben als Referenzwert
  string = 'c' # neu zugewiesen, daher kein Verweis auf das Original
  setzt Zeichenfolge
Ende
Ausgabe (a) => 'c'
setzt a => 'b'
def output2 (string) # übergeben als Referenzwert
  string.concat ('c') # wir ändern den Wert, der in der Adresse steht
  setzt Zeichenfolge
Ende
output (a) => 'bc'
setzt a => 'bc'

Jetzt in JavaScript:

var a = 'b';
Funktionsausgabe (String) {// übergeben als Wert
  string = 'c'; // einem anderen Wert zugewiesen
  console.log (string);
}
Ausgabe (a); => 'c'
console.log (a); => 'b'
Funktion output2 (string) {// übergeben von value
  string.concat ('c'); // Wir können es nicht ohne Referenz ändern
  console.log (string);
}
output2 (a); => 'b'
console.log (a); => 'b'

Wenn Sie ein Objekt (kein primitiver Typ wie wir) in JavaScript an die Funktion übergeben, funktioniert dies genauso wie im Ruby-Beispiel.

Andere Sprachen

Wir haben bereits gesehen, wie Kopieren / Übergeben nach Wert und Kopieren / Übergeben nach Referenzwert funktionieren. Jetzt werden wir sehen, worum es bei der Referenzübergabe geht, und wir werden auch entdecken, wie wir Objekte ändern können, wenn wir den Wert übergeben.

Da ich nach Referenzsprachen gesucht habe, habe ich nicht allzu viele gefunden und mich für Perl entschieden. Sehen wir uns an, wie das Kopieren in Perl funktioniert:

mein $ x = 'string';
mein $ y = $ x;
$ x = 'neuer String';
print "$ x"; => 'neuer String'
print "$ y"; => 'string'
mein $ a = {data => "string"};
mein $ b = $ a;
$ a -> {data} = "neuer String";
print "$ a -> {data} \ n"; => 'neuer String'
print "$ b -> {data} \ n"; => 'neuer String'

Nun, das scheint genauso zu sein wie in Ruby. Ich habe keinen Beweis gefunden, aber ich würde sagen, dass Perl als Referenzwert für String kopiert wird.

Lassen Sie uns nun überprüfen, was Passieren als Referenz bedeutet:

mein $ x = 'string';
print "$ x"; => 'string'
sub foo {
  $ _ [0] = 'neuer String';
  print "$ _ [0]"; => 'neuer String'
}
foo ($ x);
print "$ x"; => 'neuer String'

Da Perl als Referenz übergeben wird, wenn Sie eine Neuzuweisung innerhalb der Funktion vornehmen, wird auch der ursprüngliche Wert der Speicheradresse geändert.

Für die Übergabe der Wertesprache habe ich Go gewählt, da ich beabsichtige, mein Go-Wissen in absehbarer Zukunft zu vertiefen:

Paket main
"fmt" importieren
func changeAddress (a * int) {
  fmt.Println (a)
  * a = 0 // Setzen Sie den Wert der Speicheradresse auf 0
}
func changeValue (a int) {
  fmt.Println (a)
  a = 0 // Wir ändern den Wert innerhalb der Funktion
  fmt.Println (a)
}
func main () {
  a: = 5
  fmt.Println (a)
  fmt.Println (& a)
  changeValue (a) // a wird als Wert übergeben
  fmt.Println (a)
  changeAddress (& a) // Speicheradresse von a wird als Wert übergeben
  fmt.Println (a)
}
Wenn Sie den Code kompilieren und ausführen, erhalten Sie Folgendes:
0xc42000e328
5
5
0
5
0xc42000e328
0

Wenn Sie den Wert einer Speicheradresse ändern möchten, müssen Sie Zeiger verwenden und Speicheradressen nach Wert weitergeben. Ein Zeiger enthält die Speicheradresse eines Wertes.

Der Operator & generiert einen Zeiger auf seinen Operanden und der Operator * gibt den zugrunde liegenden Wert des Zeigers an. Dies bedeutet grundsätzlich, dass Sie mit & die Speicheradresse eines Wertes übergeben und mit * den Wert einer Speicheradresse einstellen.

Fazit

So bewerten Sie eine Sprache:

  1. Verstehen Sie die zugrunde liegenden Datentypen in der Sprache. Lesen Sie einige Spezifikationen und spielen Sie damit. Es läuft in der Regel auf primitive Typen und Objekte hinaus. Überprüfen Sie dann, ob diese Objekte veränderlich oder unveränderlich sind. Einige Sprachen verwenden unterschiedliche Kopier- / Übergabetaktiken für unterschiedliche Datentypen.
  2. Der nächste Schritt ist die Variablenzuordnung, das Kopieren, die Neuzuordnung und der Vergleich. Dies ist der wichtigste Teil, den ich denke. Sobald Sie dies erhalten haben, können Sie herausfinden, was los ist. Es ist sehr hilfreich, wenn Sie die Speicheradressen beim Herumspielen überprüfen.
  3. Das Übergeben von Variablen an Funktionen ist normalerweise keine Besonderheit. Dies funktioniert normalerweise genauso wie das Kopieren in den meisten Sprachen. Sobald Sie wissen, wie die Variablen kopiert und neu zugewiesen werden, wissen Sie bereits, wie sie an Funktionen übergeben werden.

Die hier verwendeten Sprachen:

  • Gehe zu: Kopiert und als Wert übergeben
  • JavaScript: Primitive Typen werden als Wert kopiert / übergeben, Objekte werden als Referenzwert kopiert / übergeben
  • Ruby: Kopiert und übergeben von Referenzwert + veränderlichen / unveränderlichen Objekten
  • Perl: Nach Referenzwert kopiert und als Referenz übergeben

Wenn Leute sagen, dass sie als Referenz übergeben wurden, meinen sie normalerweise, dass sie als Referenzwert übergeben wurden. Übergeben als Referenzwert bedeutet, dass Variablen als Wert übergeben werden, diese Werte jedoch Referenzen auf die Objekte sind.

Wie Sie gesehen haben, verwendet Ruby nur das Übergeben von Referenzwerten, während JavaScript eine gemischte Strategie verwendet. Aufgrund der unterschiedlichen Implementierung der Datenstrukturen ist das Verhalten jedoch für fast alle Datentypen gleich.

Die meisten gängigen Sprachen werden entweder als Wert kopiert und übergeben oder als Referenzwert kopiert und übergeben. Zum letzten Mal: ​​Referenzübergabe Der Wert wird normalerweise als Referenzübergabe bezeichnet.

Im Allgemeinen ist die Übergabe nach Wert sicherer, da Sie nicht auf Probleme stoßen, da Sie den ursprünglichen Wert nicht versehentlich ändern können. Es ist auch langsamer zu schreiben, da Sie Zeiger verwenden müssen, wenn Sie die Objekte ändern möchten.

Es ist die gleiche Idee wie beim statischen und dynamischen Schreiben - Entwicklungsgeschwindigkeit auf Kosten der Sicherheit. Wie Sie erraten haben, ist die Übergabe von Werten in der Regel eine Funktion von Sprachen niedrigerer Stufe wie C, Java oder Go.

Referenzübergabe oder Referenzwert werden normalerweise von höheren Sprachen wie JavaScript, Ruby und Python verwendet.

Wenn Sie eine neue Sprache entdecken, durchlaufen Sie den Prozess wie hier und Sie werden verstehen, wie es funktioniert.

Dies ist kein einfaches Thema und ich bin nicht sicher, ob alles richtig ist, was ich hier geschrieben habe. Wenn Sie der Meinung sind, dass ich in diesem Artikel einige Fehler gemacht habe, lassen Sie es mich bitte in den Kommentaren wissen.