Vše, co potřebujete vědět o odkazu vs hodnotou

Pokud jde o softwarové inženýrství, existuje poměrně málo nepochopených konceptů a zneužitých pojmů. Odkazem na hodnotu je rozhodně jedním z nich.

Vzpomínám si zpět v den, kdy jsem četl téma a zdálo se, že každý zdroj, kterým jsem procházel, je v rozporu s předchozím. Trvalo to nějakou dobu, než jsme to získali. Neměl jsem na výběr, protože je to základní téma, pokud jste softwarový inženýr.

Před několika týdny jsem narazil na ošklivou chybu a rozhodl jsem se napsat článek, aby ostatní lidé měli snadnější čas na vymýšlení celé věci.

Denně kóduji v Ruby. JavaScript používám také velmi často, proto jsem si pro tuto prezentaci vybral tyto dva jazyky.

K pochopení všech konceptů však použijeme také příklady Go a Perl.

Abyste pochopili celé téma, musíte pochopit 3 různé věci:

  • Jak jsou základní datové struktury implementovány v jazyce (objekty, primitivní typy, mutabilita,).
  • Jak funguje variabilní přiřazení / kopírování / opětovné přiřazení / porovnání
  • Jak jsou proměnné předávány funkcím

Základní typy dat

V Ruby neexistují žádné primitivní typy a vše je objekt, včetně celých čísel a booleovců.

A ano, v Ruby je TrueClass.

true.is_a? (TrueClass) => true
3.is_a? (Celé číslo) => true
true.is_a? (Object) => true
3.is_a? (Object) => true
TrueClass.is_a? (Object) => true
Integer.is_a? (Object) => true

Tyto objekty mohou být buď proměnlivé, nebo neměnné.

Neměnný znamená, že neexistuje žádný způsob, jak změnit objekt, jakmile bude vytvořen. Existuje pouze jedna instance pro danou hodnotu s jedním object_id a zůstává stejná bez ohledu na to, co děláte.

Implicitně jsou v Ruby neměnné typy objektů: Boolean, Numeric, nil a Symbol.

V MRI je object_id objektu stejný jako HODNOTA, která představuje objekt na úrovni C. U většiny druhů objektů je tato HODNOTA ukazatelem na umístění v paměti, kde jsou uložena skutečná objektová data.

Od této chvíle budeme zaměnitelně používat adresu object_id a paměť.

Pojďme spustit nějaký Ruby kód v MRI pro neměnný Symbol a proměnlivý řetězec:

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

Jak vidíte, zatímco verze symbolu udržuje stejný object_id pro stejnou hodnotu, hodnoty řetězce patří k různým adresám paměti.

Na rozdíl od Ruby má JavaScript primitivní typy.

Jsou - booleovské, nulové, nedefinované, String a Number.

Zbytek datových typů spadá pod deštník Objects (Array, Function a Object). Není tu nic fantastického, je to mnohem jednodušší než Ruby.

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

Proměnné přiřazení, kopírování, opětovné přiřazení a porovnání

V Ruby je každá proměnná pouze odkazem na objekt (protože vše je objektem).

a = 'řetězec'
b = a
# Pokud přiřadíte a stejnou hodnotu
a = 'řetězec'
dá b => 'řetězec'
dá a == b => true # hodnoty jsou stejné
vloží a.object_id == b.object_id => false # adr-s. lišit
# Pokud znovu přiřadíte jinou hodnotu
a = 'nový řetězec'
vloží = = 'nový řetězec'
dá b => 'řetězec'
dá a == b => false # hodnoty jsou odlišné
vloží a.object_id == b.object_id => false # adr-s. liší se také

Když přiřadíte proměnnou, jedná se o odkaz na objekt, nikoli na samotný objekt. Při kopírování objektu b = a obě proměnné ukážou na stejnou adresu.

Toto chování se nazývá kopie podle referenční hodnoty.

Přísně řečeno v Ruby a JavaScript je vše kopírováno podle hodnoty.

Pokud však jde o objekty, hodnoty jsou náhodně adresy paměti těchto objektů. Díky tomu můžeme modifikovat hodnoty, které sedí v těchto adresách paměti. Opět se tomu říká kopie podle referenční hodnoty, ale většina lidí to označuje jako kopie podle reference.

Zkopírovalo by se odkazem, pokud by po přiřazení a k „novému řetězci“ b ukázal také na stejnou adresu a měl stejnou hodnotu „nový řetězec“.

Když deklarujete b = a, a a b směřují na stejnou adresu pamětiPo přiřazení a (a = „řetězec“), aab ukazují na různé adresy paměti

Totéž s neměnným typem, jako je Integer:

a = 1
b = a
a = 1
dá b => 1
vloží a == b => true # srovnání podle hodnoty
vloží a.object_id == b.object_id => true # srovnání pomocí paměti adr.

Když přiřadíte a ke stejnému celému číslu, zůstane adresa paměti stejná, protože dané celé číslo má vždy stejný object_id.

Jak vidíte, když porovnáte jakýkoli objekt s jiným, je porovnán podle hodnoty. Pokud chcete zjistit, zda se jedná o stejný objekt, musíte použít object_id.

Uvidíme verzi JavaScriptu:

var a = 'string';
var b = a;
a = 'řetězec'; # a je přiřazena ke stejné hodnotě
console.log (a); => 'řetězec'
console.log (b); => 'řetězec'
console.log (a === b); => true // srovnání podle hodnoty
var a = [];
var b = a;
console.log (a === b); => pravda
a = [];
console.log (a); => []
console.log (b); => []
console.log (a === b); => false // porovnání s adresou paměti

Kromě porovnání - JavaScript používá hodnotu pro primitivní typy a odkaz na objekty. Chování vypadá stejně jako v Ruby.

No, ne docela.

Primitivní hodnoty v JavaScriptu nebudou sdíleny mezi více proměnnými. I když nastavíte proměnné na sebe. Je zaručeno, že každá proměnná představující primitivní hodnotu patří do jedinečného umístění v paměti.

To znamená, že žádná z proměnných nebude nikdy ukazovat na stejnou adresu paměti. Je také důležité, aby samotná hodnota byla uložena v umístění fyzické paměti.

V našem příkladu, když deklarujeme b = a, b bude okamžitě ukazovat na jinou adresu paměti se stejnou hodnotou řetězce. Nemusíte tedy znovu přiřadit bod k jiné paměťové adrese.

Tomu se říká kopírování podle hodnoty, protože nemáte přístup k adrese paměti pouze k hodnotě.

Když deklarujete a = b, je přiřazena podle hodnoty, takže aab ukazují na různé adresy paměti

Podívejme se na lepší příklad, kde na tom záleží.

Pokud v Ruby upravíme hodnotu, která sedí v adrese paměti, budou mít všechny odkazy, které na tuto adresu odkazují, stejnou aktualizovanou hodnotu:

a = 'x'
b = a
a.concat ('y')
klade a => 'xy'
dá b => 'xy'
b.concat ('z')
vloží a => 'xyz'
dá b => 'xyz'
a = 'z'
vloží a => 'z'
dá b => 'xyz'
a [0] = 'y'
dá a => 'y'
dá b => 'xyz'

Můžete si myslet, že v JavaScriptu se změní pouze hodnota a, ale ne. Nemůžete dokonce změnit původní hodnotu, protože nemáte přímý přístup k adrese paměti.

Dalo by se říci, že jste přiřazili písmeno „x“, ale bylo to přiřazeno podle hodnoty, takže adresa paměti má hodnotu „x“, ale nemůžete ji změnit, protože na ni nemáte žádný odkaz.

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

Chování objektů JavaScriptu a implementace jsou stejné jako u mutovaných objektů Ruby. Obě kopie jsou referenční hodnotou.

Primitivní typy JavaScriptu jsou kopírovány podle hodnoty. Chování je stejné jako u neměnných objektů Ruby, které jsou kopírovány referenční hodnotou.

Huh?

Opět platí, že když kopírujete něco podle hodnoty, znamená to, že nemůžete změnit (mutovat) původní hodnotu, protože neexistuje žádný odkaz na adresu paměti. Z pohledu kódu pro psaní je to stejné jako mít neměnné entity, které nemůžete mutovat.

Pokud porovnáte Ruby a JavaScript, jediným typem dat, který se ve výchozím nastavení chová jinak, je String (proto jsme ve výše uvedených příkladech použili String).

V Ruby je to zaměnitelný objekt a je zkopírován / předán referenční hodnotou, zatímco v JavaScriptu je to primitivní typ a zkopírován / předán hodnotou.

Pokud chcete klonovat (ne kopírovat) objekt, musíte to udělat explicitně v obou jazycích, abyste se ujistili, že původní objekt nebude změněn:

a = {'name': 'Kate'}
b = a.clone
b ['name'] = 'Anna'
vloží a => {: name => "Kate"}
var a = {'name': 'Kate'};
var b = {... a}; // s novou syntaxí ES6
b ['jméno'] = 'Anna';
console.log (a); => {name: "Kate"}

Je velmi důležité si to pamatovat, jinak se při vyvolání kódu vícekrát narazíte na nějaké ošklivé chyby. Dobrým příkladem by byla rekurzivní funkce, ve které objekt použijete jako argument.

Další je React (JavaScript front-end framework), kde musíte vždy předat nový objekt pro aktualizaci stavu, protože srovnání funguje na základě ID objektu.

Je to rychlejší, protože nemusíte procházet řádek po řádku, abyste zjistili, zda byl změněn.

Jak jsou proměnné předávány funkcím

Předávání proměnných funkcím funguje ve většině jazyků stejně jako kopírování stejných datových typů.

V JavaScriptu jsou primitivní typy zkopírovány a předány hodnotou a objekty jsou zkopírovány a předány referenční hodnotou.

Myslím, že to je důvod, proč lidé mluví pouze o průchodu hodnotou nebo o referenci a zdá se, že nikdy nezmínili kopírování. Myslím, že předpokládají, že kopírování funguje stejným způsobem.

a = 'b'
def výstup (řetězec) # předán referenční hodnotou
  string = 'c' # znovu přiřazen, takže žádný odkaz na originál
  klade řetězec
konec
výstup (a) => 'c'
vloží a => 'b'
def output2 (string) # předán referenční hodnotou
  string.concat ('c') # změníme hodnotu, která sedí v adrese
  klade řetězec
konec
výstup (a) => 'bc'
klade a => 'bc'

Nyní v JavaScriptu:

var a = 'b';
funkční výstup (řetězec) {// předán hodnotou
  string = 'c'; // přiřazeno jiné hodnotě
  console.log (řetězec);
}
výstup (a); => 'c'
console.log (a); => 'b'
function output2 (string) {// předáno hodnotou
  string.concat ('c'); // nemůžeme to změnit bez odkazu
  console.log (řetězec);
}
výstup2 (a); => 'b'
console.log (a); => 'b'

Pokud v JavaScriptu předáte objekt (ne primitivní typ, jako jsme to udělali), funguje to stejně jako v příkladu Ruby.

Jiné jazyky

Už jsme viděli, jak funguje kopírování / předávání podle hodnoty a kopírování / předávání podle referenční hodnoty. Nyní uvidíme, o čem kolem je reference, a také zjistíme, jak můžeme změnit objekty, pokud projdeme hodnotou.

Když jsem hledal procházení referenčními jazyky, nemohl jsem najít příliš mnoho a nakonec jsem si vybral Perla. Podívejme se, jak kopírování funguje v Perlu:

my $ x = 'string';
moje $ y = $ x;
$ x = 'nový řetězec';
tisk "$ x"; => 'nový řetězec'
tisk "$ y"; => 'řetězec'
my $ a = {data => "string"};
moje $ b = $ a;
$ a -> {data} = "nový řetězec";
tisk "$ a -> {data} \ n"; => 'nový řetězec'
tisk "$ b -> {data} \ n"; => 'nový řetězec'

Zdá se, že je to stejné jako v Ruby. Nenašel jsem žádný důkaz, ale řekl bych, že Perl je kopírován referenční hodnotou pro String.

Nyní se podíváme, co znamená projít referencí:

můj $ x = 'řetězec';
tisk "$ x"; => 'řetězec'
sub foo {
  $ _ [0] = 'nový řetězec';
  tisk "$ _ [0]"; => 'nový řetězec'
}
foo ($ x);
tisk "$ x"; => 'nový řetězec'

Protože Perl je předán odkazem, pokud v rámci funkce změníte přiřazení, změní se také původní hodnota adresy v paměti.

Pro průchod hodnotovým jazykem jsem si vybral Go, když v dohledné budoucnosti hodlám prohloubit své Go znalosti:

hlavní balíček
import "fmt"
func changeAddress (a * int) {
  fmt.Println (a)
  * a = 0 // nastavení hodnoty adresy paměti na 0
}
func changeValue (int) {
  fmt.Println (a)
  a = 0 // změníme hodnotu v rámci funkce
  fmt.Println (a)
}
func main () {
  a: = 5
  fmt.Println (a)
  fmt.Println (& a)
  changeValue (a) // a je předán hodnotou
  fmt.Println (a)
  changeAddress (& a) // adresa paměti a je předána hodnotou
  fmt.Println (a)
}
Při kompilaci a spuštění kódu získáte následující:
0xc42000e328
5
5
0
5
0xc42000e328
0

Pokud chcete změnit hodnotu adresy paměti, musíte použít ukazatele a předávat adresy paměti podle hodnoty. Ukazatel uchovává adresu paměti hodnoty.

Operátor & generuje ukazatel na svůj operand a operátor * označuje základní hodnotu ukazatele. To v podstatě znamená, že předáte adresu paměti hodnotě pomocí & a hodnotu adresy adresy nastavíte *.

Závěr

Jak hodnotit jazyk:

  1. Pochopit základní datové typy v jazyce. Přečtěte si některé specifikace a pohrávejte se s nimi. Obvykle se scvrkává na primitivní typy a objekty. Pak zkontrolujte, zda jsou tyto objekty zaměnitelné nebo neměnné. Některé jazyky používají různé taktiky kopírování / předávání pro různé typy dat.
  2. Dalším krokem je přiřazení proměnných, kopírování, opětovné přiřazení a porovnání. To je podle mě nejdůležitější část. Jakmile to získáte, budete moci zjistit, co se děje. Hodně to pomůže, když při hraní zkontrolujete adresy v paměti.
  3. Předávání proměnných funkcím obvykle není zvláštní. Obvykle funguje stejně jako kopírování ve většině jazyků. Jakmile víte, jak jsou proměnné zkopírovány a znovu přiřazeny, již víte, jak jsou předávány funkcím.

Jazyky, které jsme zde použili:

  • Přejít: Zkopírováno a předáno hodnotou
  • JavaScript: Primitivní typy jsou kopírovány / předávány hodnotou, objekty jsou kopírovány / předávány referenční hodnotou
  • Ruby: Zkopírováno a předáno referenční hodnotou + proměnné / neměnné objekty
  • Perl: Zkopírováno referenční hodnotou a předáno odkazem

Když lidé říkají, že prošli referencí, obvykle znamenají, že prošli referenční hodnotou. Předávání referenční hodnotou znamená, že proměnné jsou předávány hodnotou, ale tyto hodnoty jsou odkazy na objekty.

Jak jste viděli, Ruby používá pouze průchod referenční hodnotou, zatímco JavaScript používá smíšenou strategii. Přesto je chování stejné pro téměř všechny typy dat kvůli rozdílné implementaci datových struktur.

Většina jazyků hlavního proudu je zkopírována a předána podle hodnoty nebo zkopírována a předána podle referenční hodnoty. Naposledy: Pass by reference value se obvykle nazývá pass by reference.

Obecně je hodnota pass bezpečnější, protože se vám nevyskytnou problémy, protože původní hodnotu nemůžete náhodně změnit. Zápis je také pomalejší, protože pokud chcete změnit objekty, musíte použít ukazatele.

Je to stejná myšlenka jako u statického psaní vs. dynamického psaní - rychlost vývoje za cenu bezpečnosti. Jak jste uhodli, hodnota pass je obvykle funkcí jazyků nižší úrovně, jako je C, Java nebo Go.

Předat odkaz nebo referenční hodnotu obvykle používají jazyky vyšší úrovně, jako je JavaScript, Ruby a Python.

Když objevíte nový jazyk, projděte tento proces, jako jsme to udělali tady, a pochopíte, jak to funguje.

Toto není jednoduché téma a nejsem si jistý, že je vše v pořádku, co jsem zde napsal. Pokud si myslíte, že jsem v tomto článku udělal nějaké chyby, dejte mi prosím vědět v komentářích.