Як передаються рядки в .NET?


121

Коли я передаю функцію stringa, чи передається вказівник на вміст рядка, або вся передана рядок до функції в стеку, як би це structбуло?

Відповіді:


278

Посилання передається; однак це технічно не передається шляхом посилання. Це тонка, але дуже важлива відмінність. Розглянемо наступний код:

void DoSomething(string strLocal)
{
    strLocal = "local";
}
void Main()
{
    string strMain = "main";
    DoSomething(strMain);
    Console.WriteLine(strMain); // What gets printed?
}

Щоб зрозуміти, що тут відбувається, потрібно знати три речі:

  1. Рядки - це еталонні типи в C #.
  2. Вони також непорушні, тому щоразу, коли ти робиш щось, схоже на те, що ти змінюєш струну, ти не є. Створюється абсолютно новий рядок, на нього вказується посилання, а стара викидається.
  3. Незважаючи на те, що рядки є еталонними типами, strMainвони не передаються посиланням. Це тип посилання, але сама посилання передається за значенням . Кожен раз , коли ви передаєте параметр без refключового слова (не рахуючи outпараметрів), ви передаєте щось за значенням.

Отже, це означає, що ви ... передаєте посилання за значенням. Оскільки це тип посилання, на стек було скопійовано лише посилання. Але що це означає?

Передача посилальних типів за значенням: Ви вже робите це

Змінні C # є або еталонними типами, або типовими значеннями . Параметри C # або передаються за посиланням, або передаються за значенням . Термінологія тут є проблемою; ці звуки схожі на те саме, але їх немає.

Якщо ви refпередаєте параметр БУДЬ-якого типу, і ви не використовуєте ключове слово, ви передали його за значенням. Якщо ви передали це за вартістю, те, що ви насправді пройшли, було копією. Але якщо параметр був еталонним типом, то ви скопіювали те, що було скопійовано , а не те, на що він вказував.

Ось перший рядок Mainметоду:

string strMain = "main";

У цьому рядку ми створили дві речі: рядок зі значенням, mainзбереженим десь у пам'яті, та посилання на змінну, що називається, що strMainвказує на неї.

DoSomething(strMain);

Тепер ми передаємо це посилання на DoSomething. Ми передали це за значенням, це означає, що ми зробили копію. Це тип посилання, тож це означає, що ми скопіювали посилання, а не сам рядок. Тепер у нас є дві посилання, які кожне вказують на однакове значення в пам'яті.

Всередині кали

Ось верх DoSomethingметоду:

void DoSomething(string strLocal)

Немає refключового слова, тому є strLocalі strMainдві різні посилання, що вказують на одне значення. Якщо ми перепризначимо strLocal...

strLocal = "local";   

... ми не змінили збережене значення; ми взяли посилання, що називається, strLocalі націлили його на абсолютно новий рядок. Що відбувається, strMainколи ми робимо це? Нічого. Він все ще вказує на стару рядок.

string strMain = "main";    // Store a string, create a reference to it
DoSomething(strMain);       // Reference gets copied, copy gets re-pointed
Console.WriteLine(strMain); // The original string is still "main" 

Незмінюваність

Давайте змінимо сценарій на секунду. Уявіть, що ми працюємо не з рядками, а з деяким змінним посилальним типом, як-от створений вами клас.

class MutableThing
{
    public int ChangeMe { get; set; }
}

Якщо слідувати посиланням objLocalна об'єкт, на який вказує, ви можете змінити його властивості:

void DoSomething(MutableThing objLocal)
{
     objLocal.ChangeMe = 0;
} 

У MutableThingпам'яті залишається лише одна , і скопійована посилання, і оригінальна посилання все ще вказують на неї. Властивості самого MutableThingсебе змінилися :

void Main()
{
    var objMain = new MutableThing();
    objMain.ChangeMe = 5; 
    Console.WriteLine(objMain.ChangeMe); // it's 5 on objMain

    DoSomething(objMain);                // now it's 0 on objLocal
    Console.WriteLine(objMain.ChangeMe); // it's also 0 on objMain   
}

Ах, але струни незмінні! Немає ChangeMeвластивості встановлювати. Ви не можете робити strLocal[3] = 'H'в C #, як ви могли, з charмасивом у стилі C ; ви повинні створити зовсім новий рядок. Єдиний спосіб зміни strLocal- це наведення посилання на інший рядок, а це означає, що нічого, що ви робите, не strLocalможе вплинутиstrMain . Значення незмінне, а посилання - копія.

Проходження посилання за посиланням

Щоб довести свою різницю, ось що відбувається при передачі посилання за посиланням:

void DoSomethingByReference(ref string strLocal)
{
    strLocal = "local";
}
void Main()
{
    string strMain = "main";
    DoSomethingByReference(ref strMain);
    Console.WriteLine(strMain);          // Prints "local"
}

Цього разу рядок у Mainдійсно змінюється, тому що ви передали посилання, не копіюючи її в стек.

Отже, навіть якщо рядки є еталонними типами, передача їх за значенням означає, що все, що відбувається в абоненті, не вплине на рядок у виклику. Але оскільки вони є еталонними типами, вам не доведеться копіювати весь рядок у пам'ять, коли ви хочете передати його навколо.

Подальші ресурси:


3
@TheLight - Вибачте, але ви тут неправильно сказали: "Тип посилання передається за посиланням за замовчуванням." За замовчуванням всі параметри передаються за значенням, але для типів посилань це означає, що посилання передається за значенням. Ви плутаєте еталонні типи з еталонними параметрами, що зрозуміло, оскільки це дуже заплутане розрізнення. Дивіться розділ « Проходження посилальних типів за значенням» тут Ваша пов’язана стаття є цілком коректною, але вона насправді підтримує мою думку.
Джастін Морган

1
@JustinMorgan Не виводити мертвих ниток коментарів, але я думаю, що коментар TheLight має сенс, якщо ви думаєте на C. У C дані є лише блоком пам'яті. Посилання - це вказівник на цей блок пам'яті. Якщо ви передаєте весь блок пам'яті функції, яка називається "передача за значенням". Якщо ви передаєте вказівник, він називається "проходження посиланням". У C # немає поняття проходження у всьому блоці пам'яті, тому вони переосмислили "проходження за значенням", щоб означати передачу вказівника. Це здається неправильним, але вказівник - це теж блок пам'яті! Для мене термінологія досить умовна
rliu

@roliu - Проблема в тому, що ми не працюємо в C, а C # надзвичайно відрізняється, незважаючи на схожу назву та синтаксис. З одного боку, посилання не є такими ж, як покажчики , і мислення про них таким чином може призвести до підводних каменів. Найбільша проблема, однак, полягає в тому, що "проходження посилання" має в C # дуже специфічне значення, яке вимагає refключового слова. Щоб довести, що перехід за посиланням має значення, дивіться це демонстраційне повідомлення: rextester.com/WKBG5978
Джастін Морган

1
@JustinMorgan Я погоджуюся, що змішування термінів C і C # погано, але, хоча я насолоджувався постом ліпперта, я не погоджуюся з тим, що думка про посилання як на вказівники особливо тут не задумує. У публікації блогу описано, як мислення посилання як вказівника надає йому надто великої сили. Я знаю, що refключове слово є корисним, я просто намагався пояснити, чому можна подумати про передачу посилального типу за значенням у C #, схоже, на "традиційне" (тобто С) поняття проходження посилання (і передача посилального типу) посилання в C # здається більше схожим на передачу посилання на посилання за значенням).
rliu

2
Ви правильно, але я думаю , що @roliu було посилання , як така функція , як Foo(string bar)можна було б розглядати як Foo(char* bar)тоді Foo(ref string bar)б Foo(char** bar)(або , Foo(char*& bar)або Foo(string& bar)в C ++). Звичайно, це не так, як ви повинні думати про це щодня, але насправді це допомогло мені нарешті зрозуміти, що відбувається під капотом.
Коул Джонсон

23

Рядки в C # є незмінними опорними об'єктами. Це означає, що посилання на них передаються навколо (за значенням), і щойно створена рядок, ви не можете її змінити. Методи, що створюють модифіковані версії рядка (підрядки, обрізані версії тощо), створюють модифіковані копії вихідного рядка.


10

Струни - це особливі випадки. Кожен екземпляр незмінний. Коли ви змінюєте значення рядка, ви виділяєте нову рядок у пам'яті.

Таким чином, лише посилання передається на вашу функцію, але коли рядок редагується, вона стає новим екземпляром і не змінює старий екземпляр.


4
У цьому аспекті рядки не є особливим випадком. Створити незмінні об'єкти, які могли б мати однакову семантику, дуже легко. (Тобто екземпляр типу, який не викриває метод його мутації ...)

Рядки - це особливі випадки - вони є фактично незмінними посиланнями, які здаються незмінними, оскільки вони поводяться як типи значень.
Енігмативність

1
@ Енігмативність За цією логікою Uri(клас) та Guid(структура) також є особливими випадками. Я не бачу, як System.Stringдіє на зразок "типу значення" більше, ніж інші непорушні типи ... будь-якого класу чи структури структури.

3
@pst - У рядків є спеціальна семантика створення - на відміну від Uri& Guid- ви можете просто призначити рядково -буквальне значення рядкової змінної. Рядок здається мутабельним, як intпереназначений, але створює об'єкт неявно - немає newключового слова.
Енігмативність

3
Рядок - це особливий випадок, але це не має жодного значення для цього питання. Тип значення, тип посилання, незалежно від типу, у цьому запитанні все буде однаково.
Кірк Бродхерст
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.