Усё, што вам трэба ведаць па спасылцы супраць велічыні

Што тычыцца распрацоўкі праграмнага забеспячэння, існуе даволі шмат няправільна зразумелых канцэпцый і няправільна выкарыстаных тэрмінаў. Па спасылцы супраць па велічыні, безумоўна, адзін з іх.

Я памятаю яшчэ ў той дзень, калі я чытаў гэтую тэму, і кожная крыніца, якую я прайшоў, здавалася, супярэчыла папярэдняй. Спатрэбіўся нейкі час, каб зразумець гэта. У мяне не было выбару, бо гэта асноўны прадмет, калі вы інжынер-праграміст.

Некалькі тыдняў назад я сутыкнуўся з непрыемнай памылкай і вырашыў напісаць артыкул, каб іншым людзям было лягчэй разгадаць усё гэта.

Я кадую ў Ruby штодня. Я таксама выкарыстоўваю JavaScript даволі часта, таму для гэтай прэзентацыі я абраў гэтыя дзве мовы.

Каб зразумець усе паняцці, хаця мы будзем выкарыстоўваць і некаторыя прыклады Go і Perl.

Каб зразумець усю тэму, трэба разумець 3 розныя рэчы:

  • Як рэалізуюцца асноўныя структуры дадзеных у мове (аб'екты, прымітыўныя тыпы, змяняльнасць).
  • Як працуюць размеркаванне / капіраванне / перапрызначэнне / параўнанне
  • Як пераменныя перадаюцца функцыям

Асноўныя тыпы дадзеных

У Рубі няма прымітыўных тыпаў, і ўсё гэта аб'ект, уключаючы цэлыя лікі і булевыя.

І так, у Ruby ёсць TrueClass.

true.is_a? (TrueClass) => Праўда
3.is_a? (Цэлы лік) => праўда
true.is_a? (аб'ект) => true
3.is_a? (Аб'ект) => true
TrueClass.is_a? (Аб'ект) => true
Integer.is_a? (Аб'ект) => true

Гэтыя аб'екты могуць быць альбо нязменнымі, альбо нязменнымі.

Перарабляецца азначае, што няма магчымасці змяніць аб'ект пасля яго стварэння. Існуе толькі адзін асобнік дадзенага значэння з адным object_id, і ён застаецца такім жа, незалежна ад таго, што вы робіце.

Па змаўчанні ў Ruby нязменнымі тыпамі аб'ектаў з'яўляюцца: булевы, лікавы, нулявы і сімвал.

У МРТ object_id аб'екта супадае з VALUE, які ўяўляе аб'ект на ўзроўні C. Для большасці відаў аб'ектаў гэтая VALUE з'яўляецца паказальнікам на месца ў памяці, дзе захоўваюцца фактычныя дадзеныя аб'екта.

З гэтага часу мы будзем выкарыстоўваць object_id і адрас памяці ўзаемазаменна.

Давайце запусцім нейкі код Ruby ў МРТ для нязменнага сімвала і зменнай радкі:

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

Як вы бачыце, пакуль версія сімвала захоўвае адзін і той жа object_id для таго ж значэння, значэнні радкоў належаць да розных адрасоў памяці.

У адрозненне ад Ruby, JavaScript мае прымітыўныя тыпы.

Яны - "булевыя", "null", "undefined", "string" і "number".

Астатнія тыпы дадзеных знаходзяцца пад парасонам аб'ектаў (масіў, функцыі і аб'ект). Тут няма нічога больш мудрагелістага, чым Рубі.

[] instanceof Array => true
[] instanceof Object => праўда
3 instanceof Object => false

Зменнае прызначэнне, капіраванне, пераназначэнне і параўнанне

У Ruby кожная зменная - гэта проста спасылка на аб'ект (паколькі ўсё з'яўляецца аб'ектам).

a = 'string'
b = а
# Калі вы прысвоіце аднолькавае значэнне
a = 'string'
ставіць b => 'радок'
ставіць a == b => сапраўдныя значэнні # аднолькавыя
ставіць a.object_id == b.object_id => false # adr-s памяці. адрозніваюцца
# Пры паўторным прызначэнні іншага значэння
a = 'новая радок'
ставіць a => 'новы радок'
ставіць b => 'радок'
ставіць a == b => false # значэнні розныя
ставіць a.object_id == b.object_id => false # adr-s памяці. адрозніваюцца таксама

Пры прызначэнні зменнай, гэта спасылка на аб'ект, а не на сам аб'ект. Пры капіяванні аб'екта b = абедзве зменныя будуць паказваць на адзін і той жа адрас.

Такія паводзіны называюць копіяй па эталоннай велічыні.

Уласна кажучы, у Ruby і JavaScript усё капіюецца па значэнні.

Што тычыцца аб'ектаў, тым не менш, гэтыя значэнні з'яўляюцца адрасамі памяці гэтых аб'ектаў. Дзякуючы гэтаму мы можам змяняць значэнні, якія сядзяць у гэтых адрасах памяці. Зноў жа, гэта называецца копіяй па спасылцы, але большасць людзей называюць гэта копіяй спасылкі.

Было б копіяй спасылкі, калі б пасля прызначэння "новай радкі" b таксама паказваў бы той самы адрас і мае аднолькавае значэнне "новая радок".

Пры абвяшчэнні b = a, a і b паказваюць на той жа адрас памяціПасля прызначэння a (b = string) a і b паказваюць на розныя адрасы памяці

Тое самае з нязменным тыпам, як Integer:

a = 1
b = а
a = 1
ставіць b => 1
ставіць a == b => true # параўнанне па значэнні
ставіць a.object_id == b.object_id => true # параўнанне па адрасе памяці.

Пры паўторным прызначэнні аднаго і таго ж цэлага ліку адрас памяці застаецца аднолькавым, бо заданае цэлае лік заўсёды мае аднолькавы object_id.

Як вы бачыце, калі параўноўваць любы аб'ект з іншым, ён параўноўваецца па велічыні. Калі вы хочаце праверыць, ці з'яўляюцца яны тым самым аб'ектам, вы павінны выкарыстоўваць object_id.

Давайце паглядзім версію JavaScript:

var a = 'string';
var b = a;
a = 'string'; # a прызначаецца для таго ж значэння
console.log (a); => 'радок'
console.log (b); => 'радок'
console.log (a === b); => true // параўнанне па значэнні
var a = [];
var b = a;
console.log (a === b); => праўда
а = [];
console.log (a); => []
console.log (b); => []
console.log (a === b); => false // параўнанне па адрасе памяці

За выключэннем параўнання - JavaScript выкарыстоўвае значэнні для прымітыўных тыпаў і спасылкі на аб'екты. Паводзіны выглядае такім жа, як і ў Рубі.

Ну, не зусім.

Прымітыўныя значэнні ў JavaScript не будуць падзяляцца паміж некалькімі пераменнымі. Нават калі вы ўсталюеце пераменныя роўныя адзін аднаму. Кожная пераменная, якая прадстаўляе прымітыўнае значэнне, гарантавана належыць да ўнікальнага месца памяці.

Гэта азначае, што ні адна з пераменных ніколі не будзе паказваць на той жа адрас памяці. Важна таксама, каб само значэнне захоўвалася ў месцы фізічнай памяці.

У нашым прыкладзе, калі мы абвяшчаем b = a, b будзе адразу паказваць на іншы адрас памяці з тым жа значэннем "string". Таму вам не трэба пераназначаць пункт, каб паказаць іншы адрас памяці.

Гэта называецца скапіявана па значэнні, бо ў вас няма доступу да адраса памяці толькі да значэння.

Пры абвяшчэнні a = b яму прысвойваецца значэнне, такім чынам, a і b паказваюць на розныя адрасы памяці

Давайце разгледзім лепшы прыклад, дзе ўсё гэта мае значэнне.

У Ruby, калі мы змянім значэнне, якое знаходзіцца ў адрасе памяці, то ўсе спасылкі, якія паказваюць на адрас, будуць мець аднолькавае абноўленае значэнне:

a = 'x'
b = а
a.concat ('у')
ставіць a => 'xy'
ставіць b => 'xy'
b.concat ('z')
ставіць a => 'xyz'
ставіць b => 'xyz'
a = 'z'
ставіць a => 'z'
ставіць b => 'xyz'
a [0] = 'у'
ставіць a => 'y'
ставіць b => 'xyz'

Вы можаце падумаць, што ў JavaScript змяняецца толькі значэнне значэння, але няма. Вы нават не можаце змяніць зыходнае значэнне, бо ў вас няма прамога доступу да адраса памяці.

Вы можаце сказаць, што вы прысвоілі "х" а, але ён быў прызначаны па значэнні, таму адрас памяці мае значэнне "х", але вы не можаце змяніць яго, бо не маеце на яго ніякай спасылкі.

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

Паводзіны аб'ектаў JavaScript і іх рэалізацыя такія ж, як і зменныя аб'екты Рубі. Абедзве копіі маюць эталоннае значэнне.

Прымітыўныя тыпы JavaScript скапіруюцца па значэнні. Паводзіны гэтак жа, як і нязменныя аб'екты Рубі, скапіраваныя эталонным значэннем.

А?

Зноў жа, калі вы капіруеце што-небудзь па значэнні, гэта азначае, што вы не можаце змяніць (змяніць) зыходнае значэнне, бо няма ніякага ўказання на адрас памяці. З пункту гледжання напісання кода гэта тое самае, што мець нязменныя асобы, якія вы не можаце мутаваць.

Калі вы параўноўваеце Ruby і JavaScript, адзіным тыпам дадзеных, які па змаўчанні "паводзіць сябе", з'яўляецца String (менавіта таму мы выкарыстоўвалі String у прыкладах вышэй).

У Ruby гэта муціруемы аб'ект, і ён скапіруецца / перадаецца па эталонным значэнні, а ў JavaScript - прымітыўны тып і скапіруецца / перадаецца па значэнні.

Калі вы хочаце кланаваць (а не капіраваць) аб'ект, вы павінны зрабіць гэта яўна на абедзвюх мовах, каб пераканацца, што зыходны аб'ект не будзе зменены:

a = {'name': 'Kate'}
b = a.clone
b ['name'] = 'Ганна'
ставіць a => {: name => "Каця"}
var a = {'name': 'Kate'};
var b = {... a}; // з новым сінтаксісам ES6
b ['name'] = 'Ганна';
console.log (a); => {імя: "Kate"}

Важна памятаць пра гэта, інакш вы сутыкнецеся з нейкімі непрыемнымі памылкамі, калі яшчэ раз будзеце выклікаць свой код. Добрым прыкладам можа стаць рэкурсіўная функцыя, калі вы выкарыстоўваеце аб'ект у якасці аргумента.

Іншы - React (франтальная база JavaScript), дзе заўсёды трэба перадаць новы аб'ект для абнаўлення стану, паколькі параўнанне працуе на аснове ідэнтыфікатара аб'екта.

Гэта хутчэй, таму што вам не прыйдзецца праходзіць праз аб'ект радок за радком, каб убачыць, ці быў ён зменены.

Як пераменныя перадаюцца функцыям

Перадача зменных функцыям працуе гэтак жа, як і капіраванне для тых жа тыпаў дадзеных у большасці моў.

У JavaScript прымітыўныя тыпы капіруюцца і перадаюцца па значэнні, а аб'екты капіруюцца і перадаюцца эталонным значэннем.

Я думаю, што гэта прычына, па якой людзі кажуць толькі пра праход па кошце альбо перадаюць спасылку, і нібыта не кажуць пра капіраванне. Я мяркую, што капіраванне працуе аднолькава.

a = 'b'
вызначыць выснову (радок) # перададзена па эталонным значэнні
  string = 'c' # пераназначаны, таму няма спасылкі на арыгінал
  ставіць радок
канец
выхад (a) => 'c'
ставіць a => 'b'
def output2 (string) # перададзена па эталонным значэнні
  string.concat ('c') # мы мяняем значэнне, якое сядзіць у адрасе
  ставіць радок
канец
выхад (a) => 'bc'
ставіць a => 'bc'

Цяпер у JavaScript:

var a = 'b';
Вывад функцыі (радок) {// перадаецца па значэнні
  string = 'c'; // Пераназначаны на іншае значэнне
  console.log (string);
}
выхад (а); => 'c'
console.log (a); => 'b'
функцыя output2 (string) {// перадаецца па значэнні
  string.concat ('c'); // Мы не можам змяніць яго без спасылкі
  console.log (string);
}
выхад2 (а); => 'b'
console.log (a); => 'b'

Калі вы перадаеце аб'ект (не такі прымітыўны тып, як мы), у функцыю JavaScript ён працуе гэтак жа, як у прыкладзе Ruby.

Іншыя мовы

Мы ўжо бачылі, як працуюць капіяванне / перадача па значэнні і капіраванне / перадача па эталоннай велічыні. Зараз мы ўбачым, пра што ідзе гаворка па спасылцы, а таксама даведаемся, як мы можам змяняць аб'екты, калі праходзім па значэнні.

У пошуках пропуску на мовах спасылак я не мог знайсці занадта шмат, і я выбраў Perl. Давайце паглядзім, як працуе капіраванне ў Perl:

мой $ x = 'string';
мой $ y = $ x;
$ x = 'новая радок';
друк "$ x"; => 'новая радок'
друк "$ y"; => 'радок'
мой $ a = {data => "string"};
мой $ b = $ a;
$ a -> {data} = "Новая радок";
надрукаваць "$ a -> {дадзеныя} \ n"; => 'новая радок'
раздрукаваць "$ b -> {дадзеныя} \ n"; => 'новая радок'

Добра, што падобна на Рубі. Я не знайшоў ніякіх доказаў, але сказаў бы, што Perl скапіруецца па спасылках для String.

Зараз давайце паглядзім, што азначае перадача:

мой $ x = 'string';
друк "$ x"; => 'радок'
sub foo {
  $ _ [0] = 'Новая радок';
  раздрукаваць "$ _ [0]"; => 'новая радок'
}
foo ($ x);
друк "$ x"; => 'новая радок'

Паколькі Perl перадаецца па спасылцы, калі вы робіце пераназначэнне ў межах функцыі, гэта таксама зменіцца зыходнае значэнне адраса памяці.

Для мовы праходжання па значэнні я абраў Go, як я маю намер паглыбіць свае веды Go у агляднай будучыні:

асноўны пакет
імпарт "fmt"
func changeAddress (a * int) {
  fmt.Println (a)
  * a = 0 // Усталяванне значэння адраса памяці ў 0
}
func changeValue (інт) {
  fmt.Println (a)
  a = 0 // мы мяняем значэнне ў функцыі
  fmt.Println (a)
}
func main () {
  а: = 5
  fmt.Println (a)
  fmt.Println (& a)
  changeValue (a) // a перадаецца па значэнні
  fmt.Println (a)
  changeAddress (& a) // Адрас памяці памяці перадаецца па значэнні
  fmt.Println (a)
}
Пры кампіляцыі і запуску кода вы атрымаеце наступнае:
0xc42000e328
5
5
0
5
0xc42000e328
0

Калі вы хочаце змяніць значэнне адраса памяці, вам давядзецца скарыстацца паказальнікамі і абысці адрасы памяці па значэнні. Указальнік змяшчае значэнне адраснай памяці.

& Аператар генеруе паказальнік на свой операнд, а аператар * пазначае асноўнае значэнне паказальніка. Гэта ў асноўным азначае, што вы перадаеце адрас памяці значэння з & і вы ўсталёўваеце значэнне адраса памяці з *.

Выснова

Як ацаніць мову:

  1. Зразумець асноўныя тыпы дадзеных у мове. Прачытайце некаторыя тэхнічныя характарыстыкі і пагуляйце з імі. Звычайна яна зводзіцца да прымітыўных тыпаў і прадметаў. Затым пераканайцеся, што гэтыя аб'екты нязменныя або нязменныя. Некаторыя мовы выкарыстоўваюць розныя тактыкі капіравання / перадачы для розных тыпаў дадзеных.
  2. Наступным этапам з'яўляецца прызначэнне, капіяванне, пераназначэнне і параўнанне зменнай. Я лічу, што гэта самая важная частка. Як толькі вы атрымаеце гэта, вы зможаце зразумець, што адбываецца. Гэта вельмі дапамагае, калі вы правяраеце адрасы памяці падчас гульняў.
  3. Перадача зменных функцыям звычайна не з'яўляецца асаблівым. Звычайна гэта працуе так жа, як капіяванне на большасць моў. Як толькі вы даведаецеся, як зменныя капіяваныя і перапрызначаныя зменныя, вы ўжо ведаеце, як яны перадаюцца функцыям.

Мовы, якія мы тут выкарыстоўвалі:

  • Перайсці: Скапіявана і перададзена па значэнні
  • JavaScript: Прымітыўныя тыпы капіруюцца / перадаюцца па значэнні, аб'екты капіруюцца / перадаюцца па эталонным значэнні
  • Ruby: Скапіяваны і перададзены па эталонным значэнні + зменныя / нязменныя аб'екты
  • Perl: Скапіравана з дапамогай эталоннай велічыні і перададзена па спасылцы

Калі людзі кажуць, што прайшлі па спасылцы, яны звычайна азначаюць праходжанне эталоннай велічыні. Перадача па эталонным значэнні азначае, што зменныя перадаюцца па значэнні, але гэтыя значэнні - гэта спасылкі на аб'екты.

Як вы бачылі, Ruby выкарыстоўвае толькі праходныя значэнні, у той час як JavaScript выкарыстоўвае неадназначную стратэгію. Тым не менш, паводзіны аднолькавае практычна для ўсіх тыпаў дадзеных з-за рознай рэалізацыі структур дадзеных.

Большасць асноўных моў альбо скапіруюцца, і перадаюцца па значэнні, альбо скапіруюцца і перадаюцца па эталонным значэнні. У апошні раз: Праход па эталонным значэнні звычайна называюць праходжаннем спасылкі.

Увогуле прайсці па каштоўнасці бяспечней, бо вы не сутыкнецеся з праблемамі, бо нельга выпадкова змяніць зыходную каштоўнасць. Пісаць таксама павольней, таму што пры змене аб'ектаў вы павінны выкарыстоўваць паказальнікі.

Гэта тая самая ідэя, як і пры статычным наборы тэксту ў параўнанні з дынамічным наборам - хуткасць распрацоўкі за кошт бяспекі. Як вы ўжо здагадаліся, праход па значэнні звычайна з'яўляецца асаблівасцю моў ніжняга ўзроўню, такіх як C, Java або Go.

Прахадныя спасылкі або эталонныя значэнні звычайна выкарыстоўваюцца мовамі больш высокага ўзроўню, такімі як JavaScript, Ruby і Python.

Калі вы выявіце новую мову, прайдзіце працэс, як мы тут, і вы зразумееце, як гэта працуе.

Гэта няпростая тэма, і я не ўпэўнены, што ўсё правільна, што я напісаў тут. Калі вы лічыце, што я памыліўся ў гэтым артыкуле, калі ласка, паведаміце мне ў каментарах.