Притворяться. NET строки являются типом значения - PullRequest
7 голосов
/ 02 ноября 2009

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

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

Ответы [ 4 ]

17 голосов
/ 02 ноября 2009

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

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

(РЕДАКТИРОВАТЬ: этот раздел добавлен позже)
Когда строки передаются методу, они передаются по ссылке. Когда они читаются только в теле метода, ничего особенного не происходит. Но когда они изменяются, создается копия, и временная переменная используется в оставшейся части метода. Этот процесс называется копирование при записи .

Что мешает младшим, так это то, что они привыкли к тому, что объекты являются ссылками, и они изменяются в методе, который изменяет переданный параметр. Чтобы сделать то же самое со строками, им нужно использовать ключевое слово 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 и производительности строки

РЕДАКТИРОВАТЬ: на основе комментария от Конрада Рудольфа, я добавил этот раздел.

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

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

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

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

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

On 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 и закрепленные объекты.

Performance

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

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

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

3 голосов
/ 02 ноября 2009

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

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

3 голосов
/ 02 ноября 2009

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

1 голос
/ 02 ноября 2009

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

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

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

...