Tot el que heu de saber per referència vs per valor

Quan es tracta d’enginyeria de programari, hi ha molts conceptes malmesos i termes mal utilitzats. Per referència vs per valor, és definitivament un d’ells.

Recordo el dia en què vaig llegir el tema i cada font que vaig passar semblava contradir l'anterior. Va trigar un temps per obtenir-ne un coneixement sòlid. No he tingut cap opció, ja que és un tema fonamental si ets enginyer de programari.

Fa unes setmanes em vaig trobar amb un error desagradable i vaig decidir escriure un article perquè altres persones tinguin més temps fàcil imaginar tot això.

Codigo a Ruby cada dia. També utilitzo JavaScript prou sovint, així que he escollit aquests dos idiomes per a aquesta presentació.

Per entendre tots els conceptes, també farem servir alguns exemples de Go i Perl.

Per comprendre tot el tema heu d’entendre tres coses diferents:

  • Com s’implementen les estructures de dades subjacents al llenguatge (objectes, tipus primitius, mutabilitat,).
  • Com funciona l'assignació / còpia / reassignació / comparació de variables
  • Com es passen les variables a funcions

Tipus de dades subjacents

A Ruby no hi ha tipus primitius i tot és un objecte incloent nombres enters i booleans.

I sí, hi ha un TrueClass a Ruby.

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

Aquests objectes poden ser mutables o immutables.

Immutable significa que no hi ha cap manera de canviar l'objecte un cop creat. Hi ha una sola instància per a un valor determinat amb un object_id i es manté igual sense importar el que feu.

Per defecte a Ruby, els tipus d'objectes immutables són: Boolean, Numeric, nul i Symbol.

A la RM, l'objecte_id d'un objecte és el mateix que el VALOR que representa l'objecte al nivell C. Per a la majoria d’objectes, aquest VALUE és un punter a una ubicació de la memòria on s’emmagatzemen les dades d’objectes reals.

A partir d’ara, utilitzarem intercanviablement l’adreça object_id i la memòria.

Anem a executar alguns codis Ruby a la RM per obtenir un símbol immutable i una cadena mutable:

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

Com es veu mentre la versió del símbol manté el mateix object_id pel mateix valor, els valors de la cadena pertanyen a diferents adreces de memòria.

A diferència de Ruby, JavaScript té tipus primitius.

Són: booleà, nul, indefinit, cadena i nombre.

La resta de tipus de dades es troben sota el paraigua d'objectes (matriu, funció i objecte). No hi ha res de luxe, aquí és molt més senzill que Ruby.

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

Assignació, còpia, reassignació i comparació de variables

A Ruby, cada variable és només una referència a un objecte (ja que tot és un objecte).

a = 'cadena'
b = a
# Si reassignau una amb el mateix valor
a = 'cadena'
posa b => 'cadena'
posa a == b => els # valors reals són els mateixos
posa a.object_id == b.object_id => false # memory adr-s. difereixen
# Si reassignau un amb un altre valor
a = 'nova cadena'
posa una => "nova cadena"
posa b => 'cadena'
posa a == b => fals # els valors són diferents
posa a.object_id == b.object_id => false # memory adr-s. també difereixen

Quan assigneu una variable, és una referència a un objecte no a l'objecte en si. Quan copieu un objecte b = a ambdues variables apuntaran a la mateixa adreça.

Aquest comportament s'anomena còpia per valor de referència.

En rigor i en Ruby, tot es copia pel valor.

Tanmateix, quan es tracta d'objectes, els valors són les adreces de memòria d'aquests objectes. Gràcies a això, podem modificar els valors que queden en aquestes adreces de memòria. Un cop més, això es diu còpia per valor de referència, però la majoria de persones es refereixen a això com a còpia per referència.

Es copiaria per referència si després de reassignar-la a "nova cadena", b també apuntaria a la mateixa adreça i tindria el mateix valor "cadena nova".

Quan declares b = a, a i b apunten a la mateixa adreça de memòriaDesprés de reassignar a (a =

El mateix amb un tipus immutable com Integer:

a = 1
b = a
a = 1
posa b => 1
posa a == b => true # comparació pel valor
posa a.object_id == b.object_id => true # comparació per memòria adr.

Quan reassignau un a un mateix nombre enter, l'adreça de memòria es manté la mateixa, ja que un nombre enter determinat sempre té el mateix object_id.

Com es veu quan es compara un objecte amb un altre, es compara per valor. Si voleu comprovar si són el mateix objecte, heu d'utilitzar object_id.

Anem a veure la versió JavaScript:

var a = 'cadena';
var b = a;
a = 'cadena'; # a es reassigna al mateix valor
console.log (a); => 'cadena'
console.log (b); => 'cadena'
console.log (a === b); => true // comparació per valor
var a = [];
var b = a;
console.log (a === b); => cert
a = [];
console.log (a); => []
console.log (b); => []
console.log (a === b); => fals // comparació per adreça de memòria

Excepte la comparació: JavaScript utilitza per valor dels tipus primitius i per referència d'objectes. El comportament sembla ser el mateix que en Ruby.

Bé, no del tot.

Els valors primitius de JavaScript no es compartiran entre diverses variables. Encara que configureu les variables iguals entre si. Tota variable que representa un valor primitiu està garantida per pertànyer a una ubicació de memòria única.

Això significa que cap de les variables mai indicarà la mateixa adreça de memòria. També és important que el valor en si es guardi en una ubicació de memòria física.

En el nostre exemple quan declarem b = a, b indicarà una adreça de memòria diferent amb el mateix valor "cadena" immediatament. Per tant, no heu de tornar a assignar-ne cap per indicar una adreça de memòria diferent.

Això s’anomena copiat per valor ja que no teniu accés a l’adreça de memòria només al valor.

Quan declares a = b s’assigna per valor així que a i b apunten a diferents adreces de memòria

Anem a veure un exemple millor on tot això importa.

A Ruby si modifiquem el valor que hi ha a l’adreça de memòria, totes les referències que apunten a l’adreça tindran el mateix valor actualitzat:

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

Podeu pensar en JavaScript només canviaria el valor d'una, però no. Ni tan sols podeu canviar el valor original ja que no teniu accés directe a l'adreça de memòria.

Podríeu dir que heu assignat "x" a una, però la vau assignar per valor, de manera que una adreça de memòria de la propietat té el valor "x", però no la podeu canviar ja que no en teniu cap referència.

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

El comportament dels objectes i la implementació de JavaScript són el mateix que els d'objectes mutables de Ruby. Ambdues còpies siguin valor de referència.

Els tipus primitius de JavaScript es copien per valor. El comportament és el mateix que els objectes immutables de Ruby que es copien per valor de referència.

Oi?

Un cop més, quan copieu alguna cosa per valor vol dir que no podeu canviar (mutar) el valor original ja que no hi ha cap referència a l'adreça de memòria. Des de la perspectiva del codi d'escriptura, és el mateix que tenir entitats immutables que no pugueu mutar.

Si compareu Ruby i JavaScript, l’únic tipus de dades que “es comporta” de manera predeterminada és String (per això hem utilitzat String en els exemples anteriors).

En Ruby és un objecte mutable i es copia / passa per valor de referència mentre que en JavaScript és un tipus primitiu i es copia o passa per valor.

Quan voleu clonar (no copiar) un objecte, heu de fer-ho explícitament en els dos idiomes, així us assegureu que l’objecte original no es modificarà:

a = {'nom': "Kate"}
b = a.clone
b ['name'] = 'Anna'
posa a => {: name => "Kate"}
var a = {'nom': 'Kate'};
var b = {... a}; // amb la nova sintaxi ES6
b ['name'] = 'Anna';
console.log (a); => {nom: "Kate"}

És imprescindible recordar-ho, en cas contrari, trobareu alguns errors desagradables quan invoqueu el codi més d'una vegada. Un bon exemple seria una funció recursiva on s'utilitza l'objecte com a argument.

Un altre és Reactar (framework front-end de JavaScript) on sempre heu de passar un objecte nou per actualitzar l'estat, ja que la comparació funciona en base a l'ID d'objecte.

Això és més ràpid perquè no heu de passar per objectes línia per línia per veure si s’ha canviat.

Com es passen les variables a funcions

Passar variables a funcions funciona de la mateixa manera que còpia dels mateixos tipus de dades a la majoria dels idiomes.

A JavaScript els tipus primitius es copien i es passen per valor i els objectes es copien i es passen per valor de referència.

Crec que aquesta és la raó per la qual la gent només parla de valor per valor o de passada per referència i mai sembla mencionar còpia. Suposo que suposen copiar les obres de la mateixa manera.

a = 'b'
def output (cadena) # passat pel valor de referència
  string = 'c' # reassignat per la qual cosa no es fa referència a l'original
  posa corda
final
sortida (a) => 'c'
posa a => 'b'
def output2 (string) # passat pel valor de referència
  string.concat ('c') # canviem el valor que hi ha a l'adreça
  posa corda
final
sortida (a) => 'bc'
posa a => 'bc'

Ara a JavaScript:

var a = 'b';
sortida de la funció (cadena) {// aprovada per valor
  string = 'c'; // reassignat a un altre valor
  console.log (cadena);
}
sortida (a); => 'c'
console.log (a); => 'b'
function output2 (string) {// aprovat per valor
  string.concat ('c'); // no podem modificar-la sense referència
  console.log (cadena);
}
sortida2 (a); => 'b'
console.log (a); => 'b'

Si passa un objecte (no un tipus primitiu com ho vam fer nosaltres) a JavaScript a la funció, funciona de la mateixa manera que l'exemple Ruby.

Altres llengües

Ja hem vist com funciona la còpia / passada per valor i la còpia / passada per valor de referència. Ara veurem de què tracta el pas per referència i també descobrirem com podem canviar objectes si passem per valor.

Mentre buscava idiomes de referència, no en vaig trobar massa, i vaig acabar triant Perl. Anem a veure com funciona la còpia a Perl:

my $ x = 'string';
el meu $ y = $ x;
$ x = 'nova cadena';
imprimir "$ x"; => 'nova cadena'
imprimeix "$ y"; => 'cadena'
my $ a = {data => "string"};
els meus $ b = $ a;
$ a -> {data} = "cadena nova";
imprimiu "$ a -> {data} \ n"; => 'nova cadena'
imprimiu "$ b -> {data} \ n"; => 'nova cadena'

Doncs sembla que és el mateix que a Ruby. No he trobat cap prova però diria que Perl es copia pel valor de referència de String.

Ara comprovem què significa passar per referència:

my $ x = 'string';
imprimir "$ x"; => 'cadena'
sub foo {
  $ _ [0] = 'nova cadena';
  imprimir "$ _ [0]"; => 'nova cadena'
}
foo ($ x);
imprimir "$ x"; => 'nova cadena'

Com que Perl es passa per referència si feu una reassignació dins de la funció, canviarà també el valor original de l'adreça de memòria.

Per a un llenguatge de valor per valor he triat Go, mentre pretenc aprofundir els meus coneixements sobre el futur en un futur previsible:

principal del paquet
importa "fmt"
func changeAddress (a * int) {
  fmt.Println (a)
  * a = 0 // establint el valor de l'adreça de memòria a 0
}
func changeValue (a int) {
  fmt.Println (a)
  a = 0 // canviem el valor dins de la funció
  fmt.Println (a)
}
func main () {
  a: = 5
  fmt.Println (a)
  fmt.Println (i a)
  changeValue (a) // a es passa per valor
  fmt.Println (a)
  changeAddress (& a) // es passa per valor l'adreça de memòria de a
  fmt.Println (a)
}
Quan compileu i executeu el codi, obtindreu el següent:
0xc42000e328
5
5
0
5
0xc42000e328
0

Si voleu canviar el valor d'una adreça de memòria, heu d'utilitzar els punters i passar-los al voltant de les adreces de memòria per valor. Un punter conté l'adreça de memòria d'un valor.

El & operador genera un punter al seu operand i l'operador * denota el valor subjacent del punter. Això significa bàsicament que passeu l'adreça de memòria d'un valor amb & i definiu el valor d'una adreça de memòria amb *.

Conclusió

Com valorar un idioma:

  1. Comprendre els tipus de dades subjacents en l'idioma. Llegiu algunes especificacions i jugueu amb elles. Sol referir-se a tipus i objectes primitius. A continuació, comproveu si aquests objectes són mutables o immutables. Alguns idiomes utilitzen tàctiques de còpia / passada diferents per a diferents tipus de dades.
  2. El següent pas és l'assignació, la còpia, la reassignació i la comparació de variables. Aquesta és la part més crucial que crec. Un cop ho aconsegueixi, podràs esbrinar què passa. Hi ajuda molt si comproveu les adreces de la memòria quan es reprodueixen.
  3. Normalment, passar variables a funcions no és especial. Normalment funciona de la mateixa manera que la còpia en la majoria dels idiomes. Un cop sabeu com es copien i es reassignen les variables, ja sabeu com es passen a funcions.

Els idiomes que hem utilitzat aquí:

  • Vés: copiat i passat per valor
  • JavaScript: els tipus primitius es copien / passen per valor, els objectes es copien / es passen per valor de referència
  • Rubí: copiat i passat per valor de referència + objectes mutables / immutables
  • Perl: Copiat per valor de referència i passat per referència

Quan la gent diu passar per referència, generalment significa passar per valor de referència. Passar per valor de referència significa que les variables es transmeten per valor, però aquests valors són referències als objectes.

Com heu vist, Ruby només utilitza el valor de referència de passada, mentre que JavaScript utilitza una estratègia mixta. Tot i així, el comportament és el mateix per a gairebé tots els tipus de dades a causa de la diferent implementació de les estructures de dades.

La majoria dels idiomes principals es copien i es transmeten per valor o es copien i es transmeten per valor de referència. Per última vegada: el valor de referència per pas es sol anomenar pass per referència.

En general, el pas per valor és més segur ja que no us apareixerà en problemes ja que no podeu canviar el valor original per accident. També és més lent escriure perquè heu d'utilitzar els punters si voleu canviar els objectes.

És la mateixa idea que amb la tipificació estàtica i la tipificació dinàmica: la velocitat de desenvolupament a costa de la seguretat. Com endevinava el valor de valor, normalment és una característica de llenguatges de nivell inferior com C, Java o Go.

El valor de referència o de referència utilitzats solen utilitzar idiomes de més nivell com JavaScript, Ruby i Python.

Quan descobreixis un nou idioma, passa pel procés com ho vam fer aquí i entendràs el seu funcionament.

Aquest no és un tema fàcil i no estic segur que tot sigui correcte del que vaig escriure aquí. Si creieu que he comès alguns errors en aquest article, feu-me saber als comentaris.