Неизменяемость строки в C#

мне было любопытно, как класс StringBuilder реализуется внутри, поэтому я решил проверить исходный код Mono и сравнить его с разобранным кодом Reflector реализации Microsoft. По сути, реализация Microsoft использует char[] хранить строковое представление внутри и кучу небезопасных методов для его управления. Это прямолинейно и не вызывает никаких вопросов. Но я был смущен, когда обнаружил, что Mono использует строку внутри То StringBuilder:

private int _length;
private string _str;
public StringBuilder Append (string value) 
{
     // ...
     String.CharCopy (_str, _length, value, 0, value.Length);
}

internal static unsafe void CharCopy (char *dest, char *src, int count) 
{
    // ...
    ((short*)dest) [0] = ((short*)src) [0]; dest++; src++;
}    

Я немного программировал на C / C++, поэтому не могу сказать, что этот код меня сильно смутил, но я думал, что строки полностью неизменяемы (i.e нет абсолютно никакого способа мутировать его). Таким образом, фактические вопросы:

  • могу ли я создать полностью неизменяемый тип?
  • есть ли причина использовать такой код, кроме проблем с производительностью? (небезопасный код для изменения неизменяемых типов)
  • являются ли строки по своей сути потокобезопасными или нет?

6 ответов


могу ли я создать полностью неизменяемый тип?

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

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

есть ли причина использовать такой код, кроме проблем с производительностью?

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

  • неизменяемые структуры данных легче рассуждать, чем изменяемые структуры данных. Когда вы спрашиваете "этот список пуст?"и вы получаете ответ, тогда вы знаете, что ответ Правильный не только сейчас, но и навсегда. С изменяемыми структурами данных вы не можете спросить: "этот список пуст? Все, что вы можете спросить, Это " этот список сейчас пуст?"и тогда ответ логически отвечает на вопрос" был ли этот список пуст в какой-то момент в прошлое?"

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

void Frob(Bar bar)
{
    if (!IsSafe(bar)) throw something;
    DoSomethingDangerous(bar);
}

Если Bar является изменяемым типом, то здесь есть условие гонки; бар может быть сделан небезопасным в другом потоке после чек, но до что-то опасное происходит. Если Bar является неизменяемым типом, то ответ на вопрос остается неизменным во всем, что много более безопасный. (Представьте, если бы вы могли мутировать строку, содержащую путь после проверка безопасности, но до файл был открыт, например.)

  • методы, которые принимают неизменяемые структуры данных в качестве аргументов и возвращают их в качестве результатов и не выполняют побочных эффектов, называются "чистыми методами". Чисто методы можно memoized, который торгует увеличенной пользой памяти для увеличенной скорости, часто преогромно увеличенной скорости.

  • неизменяемые структуры данных часто могут использоваться на нескольких потоках одновременно без блокировки. Блокировка существует для предотвращения создания несогласованного состояния объекта перед мутацией, но неизменяемые объекты не имеют мутаций. (Некоторые так называемые неизменяемые структуры данных логически неизменяемы, но на самом деле делают мутации внутри себя; представьте, например, таблицу поиска, которая не изменяет свое содержимое, но реорганизует свою внутреннюю структуру если он может вывести, каким будет следующий запрос. Такая структура данных не будет автоматически потокобезопасной.)

  • неизменяемые структуры данных, которые эффективно повторно использовать их внутренние части, когда новая структура построена из старого сделать легко "сделать снимок" состояния программы, не тратя много памяти. Это делает операции undo-redo тривиальными для реализации. Это упрощает написание инструментов отладки, которые могут показать вам, как вы добрались до особое состояние программы.

  • и так далее.

являются ли строки по своей сути потокобезопасными или нет?

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

Так мне нужно использовать замки или нет?

это странный вопрос. Помните, замкикооператива. Замки работают только если все доступ к конкретному объекту согласуется со стратегией блокировки, которая должна использоваться.

вы должны использовать замки, если согласованный стратегия блокировки для доступа к конкретному объекту в конкретном место хранения-использовать замки. Если это не согласованная стратегия блокировки, то использование замков бессмысленно; вы тщательно запираете и отпираете входную дверь, в то время как кто-то другой входит в открытую заднюю дверь.

Если у вас есть строка, которая, как вы знаете, мутирует небезопасным кодом, и вы не хотите видеть несогласованные частичные мутации, и код, который делает небезопасные мутационные документы, которые он вынимает определенную блокировку во время этой мутации, то да, вам нужно использовать блокировки при доступе к этой строке. Но эта ситуация очень редка; в идеале никто не будет использовать небезопасный код для управления строкой, доступной другим кодом в другом потоке, потому что это невероятно плохая идея. Поэтому мы требуем, чтобы код, который делает это с полным доверием. И именно поэтому мы требуем, чтобы исходный код C# для такой функции размахивал большим красным флагом, который говорит: "этот код небезопасен, внимательно просмотрите его!"


Если вы идете небезопасно, можно мутировать строки в C# тоже (IIRC).


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

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

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

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

объекты, созданные из неизменяемых типов, также могут быть безопасно переданы между потоками. Поскольку они не могут быть изменены, нет никакого риска для различных потоков, меняющих их на же время, поэтому нет необходимости synchonise доступ к ним.

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


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



могу ли я создать полностью неизменяемый тип?

да. Есть конструктор для установки частных полей, получения только свойств и никаких методов.

есть ли причина использовать такой код, кроме проблем с производительностью?

один пример: такие типы не требуют безопасного использования блокировок из нескольких параллельных потоков, это упрощает запись правильного кода (нет блокировок, чтобы ошибиться).

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

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