Как строки передаются в .NET?

121

Когда я передаю stringфункции a, передается ли указатель на содержимое строки или вся строка передается функции в стеке, как если 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) понятие передачи по ссылке (и передачи ссылочного типа по ссылке в C # больше похоже на передачу ссылки на ссылку по значению).
rliu 02
2
Вы правы, но я думаю, что @roliu ссылается на то, как Foo(string bar)можно было бы думать о такой функции, как Foo(char* bar)тогда Foo(ref string bar), когда Foo(char** bar)(или Foo(char*& bar)или Foo(string& bar)в C ++). Конечно, это не то, как вы должны думать об этом каждый день, но на самом деле это помогло мне наконец понять, что происходит под капотом.
Коул Джонсон,
23

Строки в C # являются неизменяемыми ссылочными объектами. Это означает, что ссылки на них передаются (по значению), и после создания строки вы не можете ее изменить. Методы, которые создают измененные версии строки (подстроки, обрезанные версии и т. Д.), Создают измененные копии исходной строки.

dasblinkenlight
источник
10

Строки - это особые случаи. Каждый экземпляр неизменен. Когда вы меняете значение строки, вы выделяете новую строку в памяти.

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

Enigmativity
источник
4
В этом аспекте строки - не особый случай. Очень легко создать неизменяемые объекты, которые могут иметь одинаковую семантику. (То есть экземпляр типа, который не предоставляет метод для его изменения ...)
Строки - это особые случаи: они фактически являются неизменяемыми ссылочными типами, которые кажутся изменяемыми в том смысле, что ведут себя как типы значений.
Enigmativity
1
@Enigmativity По этой логике Uri(класс) и Guid(структура) также являются частными случаями. Я не понимаю, как System.Stringдействует «тип значения» больше, чем другие неизменяемые типы ... либо класса, либо происхождения структуры.
3
@pst - строки имеют особую семантику создания - в отличие от Uri& Guid- вы можете просто присвоить строковой литерал строковой переменной. Строка кажется изменяемой, как intпереназначение, но она создает объект неявно - без newключевого слова.
Enigmativity
3
String - это особый случай, но он не имеет отношения к этому вопросу. Тип значения, ссылочный тип, любой тип будут действовать одинаково в этом вопросе.
Кирк Бродхерст