Строки в dotnet являются предназначенной только для чтения последовательностью Char
-ов. Об этом явно написано в документации Microsoft, посвященной строкам. Там же в секции "Неизменность строк" сказано следующее: "Может показаться, что все методы String и операторы C# изменяют строку, но в действительности они возвращают результаты в новый строковый объект". Согласно документации, изменить строки нельзя, но жизнь не всегда согласуется с документацией, поэтому предлагаю взглянуть на способы, позволяющие изменять строки в dotnet (к тому же это иногда спрашивают на собеседованиях!).
Fixed
Нельзя изменять строки, но можно попробовать изменить то, из чего строка состоит. Как было упомянуто ранее, строка — это последовательность Char
-ов, а про неизменяемость коллекций или Char
-ов в документации информации нет. Однако, там же нет и информации о том, как получить коллекцию, на которой построена строка. Здесь будет полезно знать, что с точки зрения логики работы CLR строки — это особый тип данных. При получении указателя на объект строки возвращается указатель не на сам объект System.String
, в котором есть поле, в котором находится коллекция Char-ов, а сразу указатель на коллекцию Char-ов.
Это значит, что воспользовавшись инструкцией fixed внутри блока unsafe, можно получить указатель на первый символ строки. Путем инкрементирования или декрементирования этого указателя можно получить адрес любого символа в строке. В приведенном ниже примере изменяется второй символ в строке:
var test = "Test"
unsafe
{
fixed(char* c = test)
{
var c1=c+1;
*c1 = 'v';
}
}
Console.WriteLine(test); // Tvst
Span
Начиная с .net core 2.1 стало возможным изменить строку более изящным способом. В dotnet появилась Span-ы — абстракция для типобезопасной работы с последовательным фрагментом управляемой или неуправляемой памяти. Также у строк появился метод расширение .AsSpan()
, который позволяет получить коллекцию Char-ов в виде неизменяемого ReadOnlySpan<Char>
. Теперь необходимо конвертировать ReadOnlySpan<Char>
в Span<Char>
. Для этих целей подойдет класс MemoryMarshal, в котором есть методы:
CreateSpan
, позволяющий создать новыйSpan
для коллекции заданной длины, по ссылке на первый элемент коллекции;GetReference
, позволяющий получить ссылку на первый элемент Span-а.
var test = "Test";
var span = MemoryMarshal.CreateSpan(ref MemoryMarshal.GetReference(test.AsSpan()), test.Length);
span[1] = 'v';
Console.WriteLine(test); // Tvst
P/Invoke
В мире dotnet существует технология P/Invoke, которая помимо прочего позволяет осуществлять вызов функций в неуправляемых библиотеках из управляемого кода. Это позволяет собрать собственную динамическую библиотеку, реализованную, к примеру, на C++, и работать со строкой аналогичным с описанным в разделе Fixed образом.
Изменение строки реализуем в методе MutateSecondCharToV
, который, как следует из названия, будет заменять второй символ в строке на v
. Для создания динамической библиотеки понадобится два файла:
StringMutLib.h
#pragma once #ifdef STRINGMUTATION_EXPORTS #define STRINGMUTHLIB_API __declspec(dllexport) #else #define STRINGMUTHLIB_API __declspec(dllimport) #endif extern "C" STRINGMUTHLIB_API void MutateSecondCharToV(wchar_t* lha);
StringMutLib.cpp
STRINGMUTHLIB_API void MutateSecondCharToV(wchar_t* lha) { wchar_t* second = lha + 1; *second = 'v'; }
После того, как динамическая библиотека будет скомпилирована и положена в видимое для проекта место, останется только воспользоваться описанной в библиотеке функцией MutateSecondCharToV
:
using System.Runtime.InteropServices;
using System.Text;
var a = "Test";
Console.WriteLine(a); // Test
TestMutator.MutateSecondCharToV(a);
Console.WriteLine(a); // Tvst
public static class TestMutator
{
[DllImport("StringMutation.dll", CharSet = CharSet.Unicode)]
public static extern void MutateSecondCharToV(string foo);
}
Подробнее про данный метод, а также о том, как избежать описанных далее побочных эффектов при его использовании, рассказал широкоизвестный в узких кругах Dr. Friedrich von Never.
Побочные эффекты
После применения любого из описанных выше способов для изменения строк в программе проявится "побочный эффект": Каждый вызов Console.WriteLine("Test")
будет выводить в консоль не Test
, а Tvst
. Подобный "эффект" связан с механизмом интернирования литеральных строк.
Среда CLR поддерживает хранилище строк в виде таблицы, называемой пулом интернирования. Эта таблица содержит ссылку на каждую уникальную строку литерала, объявленную или созданную в программе.Это позволяет экземпляру литеральной строки с определенным значением встречаться в системе только один раз. Манипуляции по изменению строк из предыдущих разделов создали ситуацию, когда CLR не знает, что указатель, который раньше указывал на строку Test
, теперь указывает на Tvst
, как если бы в супермаркете кто-то поставил товар под несоответствующий ему ценник. По этой причине не стоит использовать эти методы без особой необходимости.
String.Create
Если необходимо изменять строки во время их создания с целью повышения производительности, то для этих целей подойдет метод String.Create
. Этот метод позволяет преобразовать коллекцию Char
-ов в строку, используя переданный делегат:
var buffer = new char[] { 'T', 'e', 's', 't' };
string result = string.Create(buffer.Length, buffer, (chars, buf) => {
for (int i = 0; i < chars.Length; i++)
{
if(i == 1)
{
chars[i] = 'v';
continue;
}
chars[i] = buf[i];
}
});
Console.WriteLine(result); // Tvst
В рамках делегата предоставляется доступ к Span<Char>
, который оборачивает коллекцию Char
-ов будущей строки. Благодаря тому, что строка не будет проинтернирована, этот способ не вызывает побочных эффектов, свойственных предыдущим методам.
Заключение
Вопреки тому, что в документации dotnet в отношении строк сказано, что они являются неизменяемыми, существуют разные подходы к изменению строк. Часть из рассмотренных ранее способов являются скорее "хаками" и вряд ли могут быть использованы в production коде, хотя и могут служить наглядной демонстрацией особенностей работы CLR. Последний из рассмотренных методов, напротив, является вполне законным и уместным способом изменять строки и более того может служить для оптимизации работы приложения.