de Szilard Magyar

Tot ce trebuie să știți în funcție de referință față de valoare

Tot ce trebuie sa stiti in functie de referinta fata

Când vine vorba de ingineria software, există destul de multe concepte neînțelese și termeni folosiți greșit. Prin referință versus valoare este cu siguranță unul dintre ele.

Îmi amintesc în acea zi când am citit subiectul și fiecare sursă prin care am trecut părea să o contrazică pe cea anterioară. A fost nevoie de ceva timp pentru a-l înțelege solid. Nu am avut de ales, deoarece este un subiect fundamental dacă ești inginer software.

M-am confruntat cu o problemă urâtă cu câteva săptămâni în urmă și am decis să scriu un articol, astfel încât alte persoane să aibă mai ușor timp să descopere acest lucru.

Codific în Ruby zilnic. De asemenea, folosesc JavaScript destul de des, așa că am ales aceste două limbi pentru această prezentare.

Pentru a înțelege toate conceptele, deși vom folosi și câteva exemple Go și Perl.

Pentru a înțelege întregul subiect, trebuie să înțelegeți 3 lucruri diferite:

  • Cum sunt implementate structurile de date subiacente în limbaj (obiecte, tipuri primitive, mutabilitate).
  • Cum funcționează atribuirea variabilă / copiere / reatribuire / comparație
  • Modul în care variabilele sunt transmise funcțiilor

Tipuri de date subiacente

În Ruby nu există tipuri primitive și totul este un obiect, inclusiv numere întregi și booleeni.

Și da, există un TrueClass în Ruby.

true.is_a?(TrueClass) => true3.is_a?(Integer) => truetrue.is_a?(Object) => true3.is_a?(Object) => trueTrueClass.is_a?(Object) => trueInteger.is_a?(Object) => true

Aceste obiecte pot fi fie mutabile, fie imuabile.

Imuabil înseamnă că nu există nicio modalitate de a schimba obiectul odată ce a fost creat. Există o singură instanță pentru o anumită valoare cu una object_id și rămâne la fel indiferent de ceea ce faci.

În mod implicit, în Ruby, tipurile de obiecte imuabile sunt: Boolean, Numeric, nil, și Symbol.

În RMN object_id a unui obiect este la fel ca VALUE care reprezintă obiectul de la nivelul C. Pentru majoritatea tipurilor de obiecte acest lucru VALUE este un pointer către o locație din memorie în care sunt stocate datele reale ale obiectului.

De acum înainte vom folosi object_id și memory address interschimbabil.

Să rulăm un cod Ruby în RMN pentru un simbol imuabil și un șir mutabil:

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

După cum vedeți, în timp ce versiunea simbolului păstrează același object_id pentru aceeași valoare, valorile șirului aparțin adrese de memorie diferite.

Spre deosebire de Ruby, JavaScript are tipuri primitive.

Sunt – Boolean, null, undefined, String, și Number.

Restul tipurilor de date intră sub umbrela obiectelor (Array, Function, și Object). Aici nu este nimic fantezist, este mult mai simplu decât Ruby.

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

Atribuire, copiere, reatribuire și comparație variabilă

În Ruby, fiecare variabilă este doar o referință la un obiect (deoarece totul este un obiect).

a="string"b = a
# If you reassign a with the same value
a="string"puts b => 'string'puts a == b => true # values are the sameputs a.object_id == b.object_id => false # memory adr-s. differ
# If you reassign a with another value
a="new string"puts a => 'new string'puts b => 'string'puts a == b => false # values are differentputs a.object_id == b.object_id => false # memory adr-s. differ too

Când atribuiți o variabilă, aceasta este o referință la un obiect, nu la obiectul în sine. Când copiați un obiect b = a ambele variabile vor indica aceeași adresă.

Acest comportament se numește copiază după valoarea de referință.

Vorbind strict în Ruby și JavaScript, totul este copiat în funcție de valoare.

Cu toate acestea, când vine vorba de obiecte, valorile sunt adresele de memorie ale acestor obiecte. Datorită acestui fapt putem modifica valorile care se află în acele adrese de memorie. Din nou, aceasta se numește copie după valoare de referință, dar majoritatea oamenilor se referă la aceasta ca copie prin referință.

Ar fi copiat prin referință dacă după realocare a la „șir nou”, b ar indica, de asemenea, către aceeași adresă și ar avea aceeași valoare „șir nou”.

Tot ce trebuie sa stiti in functie de referinta fata
Când declarați b = a, A și b indică aceeași adresă de memorie
1611999550 927 Tot ce trebuie sa stiti in functie de referinta fata
După reatribuire a (a = ‘șir’), A și b indică adrese de memorie diferite

La fel și cu un tip imuabil precum Integer:

a = 1b = a
a = 1puts b => 1puts a == b => true # comparison by valueputs a.object_id == b.object_id => true # comparison by memory adr.

Când reatribuiți A la același număr întreg, adresa de memorie rămâne aceeași deoarece un număr întreg are întotdeauna același obiect_id.

După cum vedeți când comparați orice obiect cu altul, acesta este comparat în funcție de valoare. Dacă doriți să verificați dacă sunt același obiect pe care trebuie să îl utilizați object_id.

Să vedem versiunea JavaScript:

var a="string";var b = a;a="string"; # a is reassigned to the same value
console.log(a); => 'string'console.log(b); => 'string'console.log(a === b); => true // comparison by value
var a = [];var b = a;
console.log(a === b); => true
a = [];
console.log(a); => []console.log(b); => []console.log(a === b); => false // comparison by memory address

Cu excepția comparației – JavaScript utilizează după valoare pentru tipurile primitive și prin referință pentru obiecte. Comportamentul pare să fie la fel ca la Ruby.

Ei bine, nu chiar.

Valorile primitive din JavaScript nu vor fi partajate între mai multe variabile. Chiar dacă setați variabilele egale între ele. Fiecare variabilă care reprezintă o valoare primitivă este garantată că aparține unei locații unice de memorie.

Aceasta înseamnă că niciuna dintre variabile nu va indica vreodată aceeași adresă de memorie. De asemenea, este important ca valoarea în sine să fie stocată într-o locație de memorie fizică.

În exemplul nostru când declarăm b = a, b va indica imediat o adresă de memorie diferită cu aceeași valoare „șir”. Deci nu trebuie să vă reatribuiți a pentru a indica o altă adresă de memorie.

Aceasta se numește copiat după valoare deoarece nu aveți acces la adresa de memorie doar la valoare.

1611999550 959 Tot ce trebuie sa stiti in functie de referinta fata
Când declarați a = b este atribuit prin valoare deci A și b indicați spre diferite adrese de memorie

Să vedem un exemplu mai bun în care toate acestea contează.

În Ruby, dacă modificăm valoarea care se află în adresa memoriei, atunci toate referințele care indică adresa vor avea aceeași valoare actualizată:

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

S-ar putea să credeți în JavaScript doar valoarea lui a s-ar schimba dar nu. Nu puteți modifica nici măcar valoarea inițială, deoarece nu aveți acces direct la adresa de memorie.

Ai putea spune că i-ai atribuit „x” a dar a fost atribuit prin valoare deci aAdresa de memorie deține valoarea ‘x’, dar nu o puteți schimba, deoarece nu aveți nicio referință la ea.

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

Comportamentul obiectelor JavaScript și implementarea sunt la fel ca obiectele mutabile Ruby. Ambele copii sunt valoare de referință.

Tipurile primitive JavaScript sunt copiate după valoare. Comportamentul este identic cu obiectele imuabile ale lui Ruby, care sunt copiate prin valoarea de referință.

Huh?

Din nou, atunci când copiați ceva în funcție de valoare, înseamnă că nu puteți modifica (muta) valoarea originală, deoarece nu există nicio referire la adresa memoriei. Din perspectiva codului de scriere, acesta este același lucru ca și cum ai avea entități imuabile pe care nu le poți muta.

Dacă comparați Ruby și JavaScript, singurul tip de date care „se comportă” diferit în mod implicit este String (de aceea am folosit String în exemplele de mai sus).

În Ruby este un obiect modificabil și este copiat / trecut prin valoare de referință în timp ce în JavaScript este un tip primitiv și copiat / trecut prin valoare.

Când doriți să clonați (nu copiați) un obiect, trebuie să îl faceți în mod explicit în ambele limbi, astfel încât să vă puteți asigura că obiectul original nu va fi modificat:

a = { 'name': 'Kate' }b = a.cloneb['name'] = 'Anna'puts a => {:name=>"Kate"}
var a = { 'name': 'Kate' };var b = {...a}; // with the new ES6 syntaxb['name'] = 'Anna';console.log(a); => {name: "Kate"}

Este crucial să vă amintiți acest lucru, altfel veți întâlni niște bug-uri urâte când vă invocați codul de mai multe ori. Un exemplu bun ar fi o funcție recursivă în care folosiți obiectul ca argument.

Un altul este React (JavaScript front-end framework) unde trebuie întotdeauna să treci un obiect nou pentru actualizare stat deoarece comparația funcționează pe baza id-ului obiectului.

Acest lucru este mai rapid, deoarece nu trebuie să parcurgeți obiectul rând cu rând pentru a vedea dacă a fost modificat.

Modul în care variabilele sunt transmise funcțiilor

Trecerea variabilelor la funcții funcționează la fel ca și copierea pentru aceleași tipuri de date în majoritatea limbilor.

În JavaScript, tipurile primitive sunt copiate și trecute prin valoare, iar obiectele sunt copiate și trecute prin valoarea de referință.

Cred că acesta este motivul pentru care oamenii vorbesc doar despre trecere prin valoare sau trecere prin referință și nu par să menționeze niciodată copierea. Cred că presupun că copierea funcționează la fel.

a="b"
def output(string) # passed by reference value  string = 'c' # reassigned so no reference to the original  puts stringend
output(a) => 'c'puts a => 'b'
def output2(string) # passed by reference value  string.concat('c') # we change the value that sits in the address  puts stringend
output(a) => 'bc'puts a => 'bc'

Acum în JavaScript:

var a="b";
function output (string) { // passed by value  string = 'c'; // reassigned to another value  console.log(string);}
output(a); => 'c'console.log(a); => 'b'
function output2 (string) { // passed by value  string.concat('c'); // we can't modify it without reference  console.log(string);}
output2(a); => 'b'console.log(a); => 'b'

Dacă treceți un obiect (nu un tip primitiv așa cum am făcut-o noi) în funcția JavaScript, funcționează la fel ca în exemplul Ruby.

Alte limbi

Am văzut deja cum funcționează copierea / trecerea prin valoare și copierea / trecerea prin valoarea de referință. Acum vom vedea despre ce trece prin referință și vom descoperi, de asemenea, cum putem schimba obiecte dacă trecem după valoare.

În timp ce căutam să trec prin limbi de referință, nu am putut găsi prea multe și am ajuns să aleg Perl. Să vedem cum funcționează copierea în Perl:

my $x = 'string';my $y = $x;$x = 'new string';
print "$x"; => 'new string'print "$y"; => 'string'
my $a = {data => "string"};my $b = $a;$a->{data} = "new string";
print "$a->{data}n"; => 'new string'print "$b->{data}n"; => 'new string'

Ei bine, acest lucru pare să fie la fel ca la Ruby. Nu am găsit nicio dovadă, dar aș spune că Perl este copiat după valoarea de referință pentru String.

Acum să verificăm ce înseamnă trecerea prin referință:

my $x = 'string';print "$x"; => 'string'
sub foo {  $_[0] = 'new string';  print "$_[0]"; => 'new string'}
foo($x);
print "$x"; => 'new string'

Din moment ce Perl este trecut prin referință dacă efectuați o realocare în cadrul funcției, va schimba și valoarea originală a adresei de memorie.

Pentru limbajul de trecere prin valoare am ales Go pentru că intenționez să aprofundez cunoștințele mele Go în viitorul apropiat:

package mainimport "fmt"
func changeAddress(a *int) {  fmt.Println(a)  *a = 0         // setting the value of the memory address to 0}
func changeValue(a int) {  fmt.Println(a)  a = 0          // we change the value within the function  fmt.Println(a)}
func main() {  a := 5  fmt.Println(a)  fmt.Println(&a)  changeValue(a) // a is passed by value  fmt.Println(a)  changeAddress(&a) // memory address of a is passed by value  fmt.Println(a)}
When you compile and run the code you will get the following:
0xc42000e32855050xc42000e3280

Dacă doriți să modificați valoarea unei adrese de memorie, trebuie să utilizați pointeri și să treceți în jurul adreselor de memorie după valoare. Un pointer deține adresa de memorie a unei valori.

& operator generează un pointer către operandul său și * operatorul indică valoarea de bază a indicatorului. Aceasta înseamnă practic că treceți adresa de memorie a unei valori cu & și setați valoarea unei adrese de memorie cu *.

Concluzie

Cum se evaluează o limbă:

  1. Înțelegeți tipurile de date subiacente în limbă. Citiți câteva specificații și jucați-vă cu ele. De obicei se reduce la tipuri și obiecte primitive. Apoi verificați dacă acele obiecte sunt mutabile sau imuabile. Unele limbi folosesc diferite tactici de copiere / trecere pentru diferite tipuri de date.
  2. Următorul pas este atribuirea variabilelor, copierea, reasignarea și compararea. Cred că este cea mai importantă parte. Odată ce obțineți acest lucru, veți putea afla ce se întâmplă. Ajută foarte mult dacă verificați adresele de memorie atunci când jucați.
  3. Trecerea variabilelor la funcții de obicei nu este specială. De obicei, funcționează la fel ca copierea în majoritatea limbilor. Odată ce știți cum sunt copiate și reatribuite variabilele, știți deja cum sunt trecute funcțiilor.

Limbile pe care le-am folosit aici:

  • Merge: Copiat și trecut prin valoare
  • JavaScript: Tipurile primitive sunt copiate / trecute prin valoare, obiectele sunt copiate / trecute după valoarea de referință
  • Rubin: Copiat și trecut prin valoarea de referință + obiecte mutabile / imuabile
  • Perl: Copiat după valoarea de referință și trecut prin referință

Când oamenii spun trecute prin referință, înseamnă de obicei trecute prin valoare de referință. Trecerea după valoarea de referință înseamnă că variabilele sunt trecute de valoare, dar acele valori sunt referințe la obiecte.

După cum ați văzut, Ruby folosește doar valoarea de referință în timp ce JavaScript utilizează o strategie mixtă. Totuși, comportamentul este același pentru aproape toate tipurile de date datorită implementării diferite a structurilor de date.

Majoritatea limbilor principale sunt fie copiate și transmise prin valoare, fie copiate și transmise prin valoarea de referință. Pentru ultima dată: Valoarea de trecere prin referință se numește de obicei trecere prin referință.

În general, trecerea prin valoare este mai sigură, deoarece nu veți întâmpina probleme, deoarece nu puteți modifica accidental valoarea inițială. De asemenea, este mai lent să scrieți, deoarece trebuie să utilizați pointeri dacă doriți să schimbați obiectele.

Este aceeași idee ca și tastarea statică vs tastarea dinamică – viteza de dezvoltare cu prețul siguranței. După cum ați ghicit, trecerea după valoare este de obicei o caracteristică a limbajelor de nivel inferior, cum ar fi C, Java sau Go.

Trecerea prin referință sau valoarea de referință sunt de obicei utilizate de limbaje de nivel superior, cum ar fi JavaScript, Ruby și Python.

Când descoperiți un nou limbaj, parcurgeți procesul așa cum am făcut-o noi aici și veți înțelege cum funcționează.

Acesta nu este un subiect ușor și nu sunt sigur că totul este corect ceea ce am scris aici. Dacă credeți că am făcut unele greșeli în acest articol, vă rog să mă anunțați în comentarii.