Притворяясь, что строки .NET являются типом значения

В .NET строки неизменяемы и являются переменными ссылочного типа. Это часто становится неожиданностью для новых разработчиков .NET, которые могут принять их за объекты типа значения из-за их поведения. Однако, кроме практики использования StringBuilder для длинной конкатенации, особенно. в циклах, есть ли на практике какая-либо причина, по которой нужно знать это различие?

Какие сценарии реального мира помогают или избегаются, если понимать различие между значениями и ссылками в отношении строк .NET и просто притворяться/неправильно понимать их как типы значений?


person Dinah    schedule 02.11.2009    source источник
comment
Почему здесь так много шума вокруг типа значения и ссылочного типа? Некоторые объекты изменяемы, некоторые неизменяемы. Концепции mutable/immutable распространяются на многие языки (даже на те, в которых нет различия между значениями и ссылками) — как только это будет понято, я думаю, что в основном это становится не академической проблемой. После этого можно обсудить типы значений MUTABLE и ссылочные типы MUTABLE (что по-прежнему совершенно тривиально после определения правил).   -  person    schedule 06.11.2009
comment
(На самом деле, я беру последнюю фразу о тривиальности в C#... arg.)   -  person    schedule 06.11.2009


Ответы (4)


Дизайн strings был преднамеренно таким, чтобы вам как программисту не нужно было слишком беспокоиться об этом. Во многих ситуациях это означает, что вы можете просто назначать, перемещать, копировать, изменять строки, не слишком задумываясь о возможных запутанных последствиях, если другая ссылка на вашу строку существует и будет изменена в то же время (как это происходит со ссылками на объекты).

Строковые параметры в вызове метода

(EDIT: этот раздел добавлен позже)
Когда строки передаются методу, они передаются по ссылке. Когда они читаются только в теле метода, ничего особенного не происходит. Но когда они изменяются, создается копия, а временная переменная используется в остальной части метода. Этот процесс называется копирование при записи.

Что беспокоит джуниоров, так это то, что они привыкли к тому, что объекты являются ссылками и они изменяются в методе, который изменяет переданный параметр. Чтобы сделать то же самое со строками, им нужно использовать ключевое слово ref. Это фактически позволяет изменить ссылку на строку и вернуть ее вызывающей функции. Если вы этого не сделаете, строка не может быть изменена телом метода:

void ChangeBad(string s)      { s = "hello world"; }
void ChangeGood(ref string s) { s = "hello world"; }

// in calling method:
string s1 = "hi";
ChangeBad(s1);       // s1 remains "hi" on return, this is often confusing
ChangeGood(ref s1);  // s1 changes to "hello world" on return

На StringBuilder

Это различие важно, но начинающим программистам обычно лучше не знать о нем слишком много. Использование StringBuilder, когда вы много "строите" строки, хорошо, но часто у вашего приложения будет гораздо больше рыбы, и небольшой прирост производительности от StringBuilder незначителен. Будьте осторожны с программистами, которые говорят вам, что все операции со строками должны выполняться с помощью StringBuilder.

В качестве очень грубого эмпирического правила: StringBuilder имеет некоторую стоимость создания, но добавление дешево. Строка имеет дешевую стоимость создания, но конкатенация относительно дорогая. Поворотный момент — около 400–500 конкатенаций, в зависимости от размера: после этого StringBuilder становится более эффективным.

Подробнее о производительности StringBuilder и строк

EDIT: основываясь на комментарии Конрада Рудольфа, я добавил этот раздел.

Если предыдущее эмпирическое правило заставляет вас задуматься, рассмотрите следующие более подробные объяснения:

  • StringBuilder с большим количеством небольших добавлений строк довольно быстро опережает конкатенацию строк (30, 50 добавлений), но на 2 мкс даже 100% прирост производительности часто незначителен (безопасен для некоторых редких ситуаций);
  • StringBuilder с добавлением некоторых больших строк (80 символов или более строк) опережает конкатенацию строк только после тысяч, иногда сотых тысяч итераций, и разница часто составляет всего несколько процентов;
  • Смешивание строковых действий (замена, вставка, подстрока, регулярное выражение и т. д.) часто делает использование StringBuilder или конкатенацию строк равными;
  • Конкатенация строк констант может быть оптимизирована компилятором, CLR или JIT, но не для StringBuilder;
  • Код часто смешивает конкатенацию +, StringBuilder.Append, String.Format, ToString и другие строковые операции, использование StringBuilder в таких случаях вряд ли когда-либо эффективно.

Итак, когда это эффективно? В тех случаях, когда добавляется много небольших строк, например, для сериализации данных в файл, и когда вам не нужно изменять «записанные» данные после «записи» в StringBuilder. И в тех случаях, когда многим методам нужно что-то дописать, потому что StringBuilder — это ссылочный тип и строки копируются при их изменении.

На интернированных струнах

Проблема возникает не только у младших программистов, когда они пытаются провести эталонное сравнение и обнаруживают, что иногда результат верен, а иногда нет, в, казалось бы, одних и тех же ситуациях. Что случилось? Когда строки были интернированы компилятором и добавлены в глобальный статический интернированный пул строк, сравнение между двумя строками может указывать на один и тот же адрес памяти. Когда (ссылка!) Сравнение двух равных строк, одной интернированной и одной нет, даст false. Используйте сравнение = или Equals и не экспериментируйте с ReferenceEquals при работе со строками.

На String.Empty

В эту же лигу вписывается странное поведение, которое иногда возникает при использовании String.Empty: статическое String.Empty всегда интернируется, а переменная с присвоенным значением — нет. Однако по умолчанию компилятор назначит String.Empty и укажет на тот же адрес памяти. Результат: изменяемая строковая переменная при сравнении с ReferenceEquals возвращает true, в то время как вместо этого можно ожидать false.

// emptiness is treated differently:
string empty1 = String.Empty;
string empty2 = "";
string nonEmpty1 = "something";
string nonEmpty2 = "something";

// yields false (debug) true (release)
bool compareNonEmpty = object.ReferenceEquals(nonEmpty1, nonEmpty2);

// yields true (debug) false (release, depends on .NET version and how it's assigned)
bool compareEmpty = object.ReferenceEquals(empty1, empty2);

Глубоко

Вы в основном спрашивали о том, какие ситуации могут возникнуть у непосвященных. Я думаю, что моя точка зрения сводится к тому, чтобы избегать object.ReferenceEquals, потому что ему нельзя доверять при использовании со строками. Причина в том, что интернирование строк используется, когда строка постоянна в коде, но не всегда. Вы не можете полагаться на это поведение. Хотя String.Empty и "" всегда интернированы, это не так, когда компилятор считает, что значение можно изменить. Различные варианты оптимизации (отладка или выпуск и другие) приведут к разным результатам.

Когда нужна необходимость ReferenceEquals в любом случае? С объектами это имеет смысл, а со строками — нет. Научите всех, кто работает со строками, избегать их использования, если они также не понимают unsafe и закрепленные объекты.

Представление

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

Большая часть использованной здесь информации подробно описана в этой прекрасной статье о строках. , а также инструкции по работе со строками на месте (изменяемые строки).

Обновление: добавлен образец кода
Обновление: добавлен раздел «Подробно» (надеюсь, кому-то это будет полезно;)
Обновление:< /strong> добавлено несколько ссылок, добавлен раздел по строковым параметрам
Обновление: добавлена ​​оценка того, когда переключаться со строк на конструктор строк
Обновление: добавлен дополнительный раздел о производительности StringBuilder и String после замечания Конрада Рудольфа

person Abel    schedule 02.11.2009
comment
Поворотный момент в производительности конкатенации шокирует меня. Я бы подозревал, что это на два порядка меньше: около 4, а не 400 конкатенаций. Глядя на эти результаты профилирования, я не могу не задаться вопросом, почему StringBuilder оказался таким неэффективным. - person Konrad Rudolph; 10.11.2009
comment
Многих это шокирует, а многие даже не верят в это. Поворотный момент может сильно различаться. При использовании concat + с большими строками точка поворота может наступить только через несколько 1000, при использовании concat с очень маленькими строками точка поворота может наступить после 50 или около того. Смешивание с заменой, регулярным выражением или Insert приводит к тому, что оба они работают примерно одинаково. Структура кода с использованием константных или динамических строк и т. д. может иметь еще одно большое влияние. - person Abel; 10.11.2009

Единственное отличие, которое действительно имеет значение для большей части кода, заключается в том, что null можно присваивать строковым переменным.

person recursive    schedule 02.11.2009

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

Когда вы копаете немного глубже и заботитесь о производительности, вы получаете реальную пользу от различия. Например, чтобы знать, что хотя передача строки в качестве параметра методу действует так, как будто создается копия строки, на самом деле копирование не происходит. Это может быть сюрпризом для людей, привыкших к языкам, в которых строки на самом деле являются типами значений (например, VB6?), и передача большого количества строк в качестве параметров не способствует производительности.

person Guffa    schedule 02.11.2009
comment
на самом деле C# (или лучше: .NET) использует copy-on-write для строк. Это означает, что если вы передаете его как параметр, ссылка действительно передается, но как только вы пытаетесь изменить ее, создается локальная копия, и ей присваивается новое значение. Переданный строковый параметр остается нетронутым. Используйте ref для строк, если вы хотите, чтобы параметр также изменился. - person Abel; 02.11.2009

Стринг – это особая порода. Они относятся к ссылочному типу, но используются большинством программистов в качестве типа значения. Делая его неизменяемым и используя внутренний пул, он оптимизирует использование памяти, которое будет огромным, если это чистый тип значения.

Подробнее здесь:
C# .NET String объект действительно по ссылке ? на SO
Метод String.Intern на MSDN
строка (справочник по C#) в MSDN

Обновление:
см. комментарий abel к этому сообщению. Это исправило мое вводящее в заблуждение утверждение.

person o.k.w    schedule 02.11.2009
comment
оптимизирует использование памяти, которое будет огромным, если это чистый тип значения.? Боюсь, скорее наоборот. Строки не оптимизированы и уж точно не для использования памяти. String.Intern используется только изредка, когда строка считается постоянной (не означает неизменяемую, но означает, что ей не присвоено другое значение). Во всех остальных случаях строки многократно копируются в память при переназначении или изменении, что плохо сказывается на производительности. - person Abel; 02.11.2009
comment
@Абель: ты прав. Я все равно оставлю свой ответ здесь, чтобы другие могли прочитать ваш полезный комментарий. Если, конечно, вы не захотите ответить :) - person o.k.w; 02.11.2009
comment
Можно и то, и другое, просто решил немного уточнить :) - person Abel; 02.11.2009