Всичко, което трябва да знаете за справка спрямо стойност

Що се отнася до софтуерното инженерство, има доста неразбрани понятия и неправилно използвани термини. За сравнение спрямо стойност определено е един от тях.

Спомням си, че в деня, когато четох по темата и всеки източник, през който минах, изглежда противоречи на предишния. Отне известно време, за да разберем здраво. Нямах избор, тъй като е основен предмет, ако сте софтуерен инженер.

Няколко седмици назад се натъкнах на гадна грешка и реших да напиша статия, за да може други хора да имат по-лесно време да разберат всичко това.

Кодирам в Ruby ежедневно. Аз също използвам JavaScript доста често, затова избрах тези два езика за тази презентация.

За да разберем всички понятия, въпреки че ще използваме и някои Go и Perl примери.

За да разберете цялата тема, трябва да разберете 3 различни неща:

  • Как се изпълняват основните структури на данни в езика (обекти, примитивни типове, мутабилност,).
  • Как работят присвояване / копиране / преназначаване / сравняване на променливи
  • Как се променят променливите към функциите

Основни типове данни

В Ruby няма примитивни типове и всичко е обект, включително цели числа и булеви знаци.

И да, в Ruby има TrueClass.

true.is_a? (TrueClass) => true
3.is_a? (Цяло число) => вярно
true.is_a? (Object) => true
3.is_a? (Обект) => true
TrueClass.is_a? (Обект) => true
Integer.is_a? (Object) => true

Тези обекти могат да бъдат или променливи, или неизменни.

Непроменливо означава, че няма начин да промените обекта, след като той е създаден. Има само един екземпляр за дадена стойност с един object_id и той остава един и същ, независимо какво правите.

По подразбиране в Ruby неизменните типове обекти са: Boolean, Numeric, nil и Symbol.

В MRI object_id на обект е същият като VALUE, който представлява обекта на ниво C. За повечето видове обекти тази VALUE е указател към местоположение в паметта, където се съхраняват действителните данни за обекта.

Отсега нататък ще използваме взаимозаменяемо object_id и адрес на паметта.

Нека стартираме малко код на Ruby в MRI за неизменим символ и мутантна струна:

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

Както виждате, докато версията на символа запазва един и същ object_id за една и съща стойност, низовите стойности принадлежат към различни адреси на паметта.

За разлика от Ruby, JavaScript има примитивни типове.

Те са - Boolean, null, undefined, String и Number.

Останалите типове данни отиват под чадъра на обекти (масив, функция и обект). Тук няма нищо фантазия, това е много по-ясно от Руби.

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

Променливо възлагане, копиране, преназначаване и сравнение

В Ruby всяка променлива е само препратка към обект (тъй като всичко е обект).

a = 'низ "
b = a
# Ако преназначавате a със същата стойност
a = 'низ "
поставя b => 'string "
поставя a == b => истинските # стойности са еднакви
поставя a.object_id == b.object_id => false # adr-s памет. различават
# Ако преназначите a с друга стойност
a = 'нов низ'
поставя a => 'нов низ "
поставя b => 'string "
поставя a == b => false # стойностите са различни
поставя a.object_id == b.object_id => false # adr-s памет. също се различават

Когато присвоите променлива, това е препратка към обект, а не към самия обект. Когато копирате обект b = и двете променливи ще сочат към един и същ адрес.

Това поведение се нарича копиране по референтна стойност.

Строго погледнато в Ruby и JavaScript всичко се копира по стойност.

Когато става въпрос за обекти обаче, стойностите са адресите на паметта на тези обекти. Благодарение на това можем да променяме стойности, които се намират в тези адреси на паметта. Отново това се нарича копие по референтна стойност, но повечето хора го наричат ​​копие по позоваване.

Това ще бъде копие чрез препратка, ако след преназначаване на „нов низ“, b също ще посочи същия адрес и ще има същата стойност „нов низ“.

Когато декларирате b = a, a и b сочат към същия адрес на паметтаСлед преназначаването на a (a = string), a и b сочат към различни адреси на паметта

Същото е с неизменим тип като Integer:

a = 1
b = a
a = 1
поставя b => 1
поставя a == b => true # сравнение по стойност
поставя a.object_id == b.object_id => true # сравнение по памет adr.

Когато пренасочите a към едно и също цяло число, адресът на паметта остава същият, тъй като дадено цяло число винаги има един и същ object_id.

Както виждате, когато сравнявате някой обект с друг, той се сравнява по стойност. Ако искате да проверите дали те са един и същ обект, трябва да използвате object_id.

Да видим версията на JavaScript:

var a = 'string';
var b = a;
a = 'низ'; # a се преназначава на същата стойност
console.log (а); => 'низ'
console.log (б); => 'низ'
console.log (a === b); => true // сравнение по стойност
var a = [];
var b = a;
console.log (a === b); => вярно
a = [];
console.log (а); => []
console.log (б); => []
console.log (a === b); => false // сравнение по адрес на паметта

С изключение на сравнението - JavaScript използва по стойност за примитивни типове и по отношение на обектите. Поведението изглежда същото като в Руби.

Е, не съвсем.

Примитивните стойности в JavaScript няма да бъдат споделени между множество променливи. Дори ако зададете променливите равни една на друга. Всяка променлива, представляваща примитивна стойност, гарантирано принадлежи на уникално място в паметта.

Това означава, че никоя от променливите никога няма да сочи към същия адрес в паметта. Също така е важно самата стойност да се съхранява във физическа памет.

В нашия пример, когато декларираме b = a, b веднага ще посочи различен адрес в паметта със същата стойност на „низ“. Затова не е необходимо да преназначавате a, за да сочите към различен адрес в паметта.

Това се нарича копирано по стойност, тъй като нямате достъп до адреса на паметта само до стойността.

Когато декларирате a = b, той се присвоява по стойност, така че a и b сочат към различни адреси на паметта

Нека видим по-добър пример, когато всичко това има значение.

В Ruby ако променим стойността, която седи в адреса на паметта, всички препратки, които сочат към адреса, ще имат същата актуализирана стойност:

a = 'x'
b = a
a.concat ( "Y")
поставя a => 'xy'
поставя b => 'xy'
b.concat ( "Z")
поставя a => 'xyz'
поставя b => 'xyz'
a = 'z'
поставя a => 'z'
поставя b => 'xyz'
a [0] = 'у'
поставя a => 'y "
поставя b => 'xyz'

Може да мислите в JavaScript само стойността на a ще се промени, но не. Не можете дори да промените първоначалната стойност, тъй като нямате директен достъп до адреса на паметта.

Можете да кажете, че сте задали „x“ на а, но той е бил присвоен по стойност, така че адресът в паметта съдържа стойността „x“, но не можете да го промените, тъй като нямате препратка към него.

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

Поведението на JavaScript обектите и тяхното изпълнение са същите като изменящите се обекти на Ruby. И двете копия са референтна стойност.

Примитивните типове JavaScript се копират по стойност. Поведението е същото като неизменните обекти на Руби, които се копират от референтната стойност.

А?

Отново, когато копирате нещо по стойност, това означава, че не можете да промените (мутирате) първоначалната стойност, тъй като няма препратка към адреса на паметта. От гледна точка на кода за писане това е същото нещо като да имаш неизменни единици, които не можеш да мутираш.

Ако сравнявате Ruby и JavaScript, единственият тип данни, който „се държи“ по подразбиране е String (затова използвахме String в примерите по-горе).

В Ruby това е изменяем обект и той се копира / предава по референтна стойност, докато в JavaScript е примитивен тип и се копира / преминава по стойност.

Когато искате да клонирате (не копирате) обект, трябва да го направите изрично на двата езика, за да сте сигурни, че оригиналният обект няма да бъде променен:

a = {'name': 'Kate'}
b = a.clone
b ['име'] = 'Анна'
поставя a => {: name => "Kate"}
var a = {'name': 'Kate'};
var b = {... a}; // с новия синтаксис ES6
b ['име'] = 'Анна';
console.log (а); => {name: "Kate"}

Важно е да запомните това, в противен случай ще се сблъскате с някои гадни грешки, когато извиквате кода си повече от веднъж. Добър пример би била рекурсивна функция, при която използвате обекта като аргумент.

Друг е React (JavaScript front-end Framework), където винаги трябва да предавате нов обект за актуализиране на състоянието, тъй като сравнението работи въз основа на идентификатор на обект.

Това е по-бързо, защото не е нужно да минавате през обект по ред, за да видите дали е променен.

Как се променят променливите към функциите

Предаването на променливи на функции работи по същия начин като копирането за същите типове данни в повечето от езиците.

В JavaScript примитивните типове се копират и предават по стойност, а обектите се копират и предават по референтна стойност.

Мисля, че това е причината, поради която хората говорят само за преминаване по стойност или за справка и никога не споменават за копиране. Предполагам, че предполагат, че копирането работи по същия начин.

a = 'b'
def изход (низ) # премина от референтната стойност
  string = 'c' # преназначен, така че няма препратка към оригинала
  поставя низ
край
изход (a) => 'c'
поставя a => 'b'
def output2 (string) # премина от референтната стойност
  string.concat ('c') # ние променяме стойността, която седи в адреса
  поставя низ
край
изход (a) => 'bc'
поставя a => 'bc'

Сега в JavaScript:

var a = 'b';
изход на функция (низ) {// предаден по стойност
  string = 'c'; // преназначен за друга стойност
  console.log (стринг);
}
изход (а); => 'c'
console.log (а); => 'b'
функция output2 (string) {// преминава по стойност
  string.concat ( "С"); // не можем да го модифицираме без препратка
  console.log (стринг);
}
output2 (а); => 'b'
console.log (а); => 'b'

Ако прехвърлите обект (не примитивен тип, какъвто го направихме) в JavaScript, към функцията, той работи по същия начин като примера на Ruby.

Други езици

Вече видяхме как работят копирането / преминаването по стойност и копирането / преминаването по референтна стойност. Сега ще видим за какво става въпрос за справка и също така ще открием как можем да променяме обекти, ако минаваме по стойност.

Докато търсех пропуск по референтни езици, не можах да намеря твърде много и в крайна сметка избрах Perl. Нека да видим как работи копирането в Perl:

my $ x = 'string';
моят $ y = $ x;
$ x = 'нов низ';
отпечатайте "$ x"; => 'нов низ'
отпечатайте "$ y"; => 'низ'
my $ a = {data => "string"};
моят $ b = $ a;
$ a -> {data} = "нов низ";
отпечатайте "$ a -> {data} \ n"; => 'нов низ'
отпечатайте "$ b -> {данни} \ n"; => 'нов низ'

Ами това изглежда е същото, както в Руби. Не намерих доказателство, но бих казал, че Perl е копиран с референтна стойност за String.

Сега нека да проверим какво означава преминаването чрез справка:

my $ x = 'string';
отпечатайте "$ x"; => 'низ'
sub foo {
  $ _ [0] = 'нов низ';
  отпечатайте "$ _ [0]"; => 'нов низ'
}
Фу ($ х);
отпечатайте "$ x"; => 'нов низ'

Тъй като Perl се предава чрез препратка, ако извършите преназначаване в рамките на функцията, това ще промени и първоначалната стойност на адреса на паметта.

За език за преминаване по ценност избрах Go, тъй като възнамерявам да задълбоча знанията си за Go в обозримо бъдеще:

основен пакет
import "fmt"
func changeAddress (a * int) {
  fmt.Println (а)
  * a = 0 // задаване на стойността на адреса на паметта на 0
}
func changeValue (int) {
  fmt.Println (а)
  a = 0 // променяме стойността в рамките на функцията
  fmt.Println (а)
}
func main () {
  a: = 5
  fmt.Println (а)
  fmt.Println (& а)
  changeValue (a) // a се предава по стойност
  fmt.Println (а)
  changeAddress (& a) // адресът на паметта на a се предава по стойност
  fmt.Println (а)
}
Когато компилирате и стартирате кода, ще получите следното:
0xc42000e328
5
5
0
5
0xc42000e328
0

Ако искате да промените стойността на адреса на паметта, трябва да използвате указатели и да обходите адресите на паметта по стойност. Показалец държи адреса на паметта на стойност.

& Операторът генерира указател към операнда си и * операторът обозначава основната стойност на показалеца. Това основно означава, че предавате адреса на паметта на стойност с & и задавате стойността на адреса на паметта с *.

заключение

Как да оценим език:

  1. Разберете основните типове данни в езика. Прочетете някои спецификации и си поиграйте с тях. Обикновено се свежда до примитивни типове и предмети. След това проверете дали тези обекти са изменчиви или неизменни. Някои езици използват различни тактики за копиране / предаване за различни типове данни.
  2. Следваща стъпка е присвояване на променлива, копиране, преназначаване и сравнение. Това е най-важната част според мен. След като постигнете това, ще можете да разберете какво се случва. Помага много, ако проверите адресите на паметта, когато играете наоколо.
  3. Предаването на променливи на функции обикновено не е специално. Обикновено работи по същия начин като копирането на повечето езици. След като разберете как се копират и преназначават променливите, вече знаете как те се предават на функции.

Езиците, които използвахме тук:

  • Go: Копирано и предадено по стойност
  • JavaScript: Примитивните типове се копират / предават по стойност, обектите се копират / предават по референтна стойност
  • Ruby: Копира се и се предава по референтна стойност + мутационни / неизменни обекти
  • Perl: Копирано от референтната стойност и предадено чрез препратка

Когато хората казват, че са преминали по референция, те обикновено означават преминат по референтна стойност. Преминаването по референтна стойност означава, че променливите се предават по стойност, но тези стойности са препратки към обектите.

Както видяхте Ruby използва само pass by reference value, докато JavaScript използва смесена стратегия. И все пак поведението е едно и също за почти всички типове данни поради различното внедряване на структурите от данни.

Повечето от основните езици са или копирани и предадени по стойност, или копирани, и предадени по референтна стойност. За последно: Pass by reference value обикновено се нарича pass by reference.

Като цяло преминаването по стойност е по-безопасно, тъй като няма да се сблъскате с проблеми, тъй като не можете случайно да промените първоначалната стойност. Също така е по-бавно да пишете, защото трябва да използвате указатели, ако искате да промените обектите.

Това е същата идея като при статично писане срещу динамично писане - скорост на разработка с цената на безопасност. Както се досещате преминаването по стойност обикновено е функция на езици от по-ниско ниво като C, Java или Go.

Pass по референтна или референтна стойност обикновено се използват от езици от по-високо ниво като JavaScript, Ruby и Python.

Когато откриете нов език, преминете през процеса, както ние направихме тук, и ще разберете как работи.

Това не е лесна тема и не съм сигурен, че всичко е правилно това, което написах тук. Ако смятате, че съм направил някои грешки в тази статия, моля, уведомете ме в коментарите.