Viss, kas jums jāzina pēc atsauces un vērtības

Runājot par programmatūras inženieriju, ir diezgan daudz pārprastu jēdzienu un nepareizi lietotu terminu. Atsauce pret vērtību noteikti ir viena no tām.

Es atceros tālajā dienā, kad es izlasīju tēmu un katrs avots, kuru es pārdzīvoju, šķita pretrunā ar iepriekšējo. Pagāja zināms laiks, lai to labi saprastu. Man nebija citas izvēles, jo tas ir fundamentāls priekšmets, ja esat programmatūras inženieris.

Dažas nedēļas atpakaļ es saskāros ar nejauku kļūdu un nolēmu uzrakstīt rakstu, lai citiem cilvēkiem varētu būt vieglāk izdomāt visu šo lietu.

Es ikdienā kodēju Ruby. Es arī diezgan bieži lietoju JavaScript, tāpēc šai prezentācijai esmu izvēlējies šīs divas valodas.

Lai saprastu visus jēdzienus, mēs izmantosim arī dažus Go un Perl piemērus.

Lai aptvertu visu tēmu, jums ir jāsaprot 3 dažādas lietas:

  • Kā valodā tiek ieviestas pamatā esošās datu struktūras (objekti, primitīvie veidi, mainīgums).
  • Kā darbojas mainīgā piešķiršana / kopēšana / mainīšana / salīdzināšana
  • Kā mainīgie tiek nodoti funkcijām

Datu tipi

Rubīnā nav primitīvu tipu, un viss ir objekts, ieskaitot veselus skaitļus un būla skaitļus.

Un jā TrueClass, Rubīnā ir a .

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

Šie objekti var būt vai nu maināmi, vai nemaināmi.

Nemaināms nozīmē, ka jūs nevarat mainīt objektu, kad tas ir izveidots. Dotajai vērtībai ir tikai viens gadījums ar vienu, object_idun tas paliek nemainīgs neatkarīgi no tā, ko jūs darāt.

Pēc noklusējuma Ruby kā negrozāma objektu veidi ir: Boolean, Numeric, nil, un Symbol.

MRI object_id objekts ir tāds pats kā tas, VALUE kas pārstāv objektu C līmenī. Vairumam objektu veidu tas VALUE norāda uz vietu atmiņā, kur tiek glabāti faktiskie objekta dati.

Turpmāk mēs to izmantosim object_idun memory addressaizstāsimies.

Palaidīsim kādu Ruby kodu MRI nemainīgam simbolam un maināmai virknei:

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

Kā redzat, kamēr simbola versija saglabā to pašu objekta ID vienai un tai pašai vērtībai, virknes vērtības pieder dažādām atmiņas adresēm.

Atšķirībā no Rubīna, JavaScript ir primitīvi tipi.

Tie ir - Boolean, null, undefined, String, un Number.

No datu veidiem pārējo iet paspārnē objektu ( Array, Function, un Object). Nekas fancy šeit tas ir daudz vairāk vienkāršāka nekā Ruby.

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

Mainīga piešķiršana, kopēšana, atkārtota piešķiršana un salīdzināšana

Rubīnā katrs mainīgais ir tikai atsauce uz objektu (jo viss ir objekts).

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

Kad piešķirat mainīgo, tā ir atsauce uz objektu, nevis pašu objektu. Kopējot objektu, b = aabi mainīgie norādīs uz to pašu adresi.

Šo rīcību sauc par kopiju pēc atsauces vērtības .

Stingri runājot Ruby un JavaScript, viss tiek kopēts pēc vērtības.

Tomēr, runājot par objektiem, vērtības ir šo objektu atmiņas adreses. Pateicoties tam, mēs varam modificēt vērtības, kas atrodas šajās atmiņas adresēs. Atkal to sauc par kopiju pēc atsauces vērtības, bet lielākā daļa cilvēku to sauc par kopiju pēc atsauces.

Tas būtu kopija ar atsauci, ja pēc atkārtotas piešķiršanas auz “jauna virkne” barī norādītu uz to pašu adresi un tai būtu tāda pati vērtība “jauna virkne”.

Tas pats ar nemaināmu veidu, piemēram, 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.

Piešķirot a tam pašam veselam skaitlim, atmiņas adrese paliek nemainīga, jo dotajam skaitlim vienmēr ir viens un tas pats object_id.

Kā redzat, salīdzinot jebkuru objektu ar citu, tas tiek salīdzināts pēc vērtības. Ja vēlaties pārbaudīt, vai tie ir tie paši objekti, kas jums jāizmantoobject_id.

Apskatīsim JavaScript versiju:

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

Izņemot salīdzinājumu - JavaScript primitīvu tipu izmanto pēc vērtības un objektiem - ar atsauci. Izskatās, ka uzvedība ir tāda pati kā Rubīnā.

Nu, ne gluži.

Primitive values in JavaScript will not be shared between multiple variables . Even if you set the variables equal to each other. Every variable representing a primitive value is guaranteed to belong to a unique memory location.

This means none of the variables will ever point to the same memory address. It is also important that the value itself is stored in a physical memory location.

In our example when we declare b = a, b will point to a different memory address with the same ‘string’ value right away. So you don’t need to reassign a to point to a different memory address.

This is called copied by value since you have no access to the memory address only to the value.

Let’s see a better example where all this matters.

In Ruby if we modify the value that sits in the memory address then all the references that point to the address will have the same updated value:

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'

You might think in JavaScript only the value of a would change but no. You can’t even change the original value as you don’t have direct access to the memory address.

You could say you assigned ‘x’ to a but it was assigned by value so a’s memory address holds the value ‘x’, but you can’t change it as you have no reference to it.

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

The behavior of JavaScript objects and implementation are the same like Ruby’s mutable objects. Both copy be reference value.

JavaScript primitive types are copied by value. The behavior is the same like Ruby’s immutable objects which are copied by reference value .

Huh?

Again, when you copy something by value it means you can’t change (mutate) the original value since there is no reference to the memory address. From the perspective of the writing code this is the same thing like having immutable entities that you can’t mutate.

If you compare Ruby and JavaScript the only data type that ‘behaves’ differently by default is String (that’s why we used String in the examples above).

In Ruby it is a mutable object and it is copied/passed by reference value while in JavaScript it is a primitive type and copied/passed by value.

When you wanna clone (not copy) an object you have to do it explicitly in both languages so you can make sure the original object won’t be modified:

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"}

It is crucial to remember this otherwise you will run into some nasty bugs when you invoke your code more than once. A good example would be a recursive function where you use the object as argument.

Another one is React (JavaScript front-end framework) where you always have to pass a new object for updating state as the comparison works based on object id.

This is faster because you don’t have to go through the object line by line to see if it has been changed.

How variables are passed to functions

Passing variables to functions is working the same way like copying for the same data types in most of the languages.

In JavaScript primitive types are copied and passed by value and objects are copied and passed by reference value.

I think this is the reason why people only talk about pass by value or pass by reference and never seem to mention copying. I guess they assume copying works the same way.

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'

Now in 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'

If you pass an object (not a primitive type like we did) in JavaScript to the function it works the same way like the Ruby example.

Other languages

We have already seen how copy/pass by value and copy/pass by reference value work. Now we will see what pass by reference is about and we also discover how we can change objects if we pass by value.

As I looked for pass by reference languages I couldn’t find too many and I ended up choosing Perl. Let’s see how copying works in 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'

Well this seems to be the same just like in Ruby. I haven’t found any proof but I would say Perl is copied by reference value for String.

Now let’s check what pass by reference means:

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

Since Perl is passed by reference if you do a reassignment within the function it will change the original value of the memory address as well.

For pass by value language I have chosen Go as I intend to deepen my Go knowledge in the foreseeable future:

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

If you wanna change the value of a memory address you have to use pointers and pass around memory addresses by value. A pointer holds the memory address of a value.

The & operator generates a pointer to its operand and the * operator denotes the pointer’s underlying value. This basically means that you pass the memory address of a value with & and you set the value of a memory address with *.

Conclusion

How to evaluate a language:

  1. Understand the underlying data types in the language. Read some specifications and play around with them. It usually boils down to primitive types and objects. Then check if those objects are mutable or immutable. Some languages use different copying/passing tactics for different data types.
  2. Next step is the variable assignment, copying, reassignment and comparison. This is the most crucial part I think. Once you get this you will be able to figure out what’s going on. It helps a lot if you check the memory addresses when playing around.
  3. Passing variables to functions usually is not special. It usually works the same way like copying in most languages. Once you you know how the variables are copied and reassigned you already know how they are passed to functions.

The languages we used here:

  • Go: Copied and passed by value
  • JavaScript: Primitive types are copied/passed by value, objects are copied/passed by reference value
  • Ruby: Copied and passed by reference value + mutable/immutable objects
  • Perl: Copied by reference value and passed by reference

When people say passed by reference they usually mean passed by reference value. Passing by reference value means that variables are passed around by value but those values are references to the objects.

As you saw Ruby only uses pass by reference value while JavaScript uses a mixed strategy. Still, the behavior is the same for almost all the data types due to the different implementation of the data structures.

Most of the mainstream languages are either copied and passed by value or copied and passed by reference value. For the last time: Pass by reference value is usually called pass by reference.

In general pass by value is safer as you won’t run into issues since you can’t accidentally change the original value. It is also slower to write because you have to use pointers if you want to change the objects.

It’s the same idea like with static typing vs dynamic typing — development speed at the cost of safety. As you guessed pass by value is usually a feature of lower level languages like C, Java or Go.

Pass by reference or reference value are usually used by higher level languages like JavaScript, Ruby and Python.

When you discover a new language go through the process like we did here and you will understand how it works.

Šī nav viegla tēma, un es neesmu pārliecināts, ka viss ir pareizi, ko es šeit rakstīju. Ja jūs domājat, ka šajā rakstā esmu pieļāvis dažas kļūdas, lūdzu, dariet to zināmu komentāros.