- PVSM.RU - https://www.pvsm.ru -

Как изменять строки в dotnet

Строки в dotnet являются предназначенной только для чтения последовательностью Char-ов. Об этом явно написано в документации Microsoft [1], посвященной строкам. Там же в секции "Неизменность строк" сказано следующее: "Может показаться, что все методы String и операторы C# изменяют строку, но в действительности они возвращают результаты в новый строковый объект". Согласно документации, изменить строки нельзя, но жизнь не всегда согласуется с документацией, поэтому предлагаю взглянуть на способы, позволяющие изменять строки в dotnet (к тому же это иногда спрашивают на собеседованиях!).


Fixed

Нельзя изменять строки, но можно попробовать изменить то, из чего строка состоит. Как было упомянуто ранее, строка — это последовательность Char-ов, а про неизменяемость коллекций [2] или Char-ов в документации информации нет. Однако, там же нет и информации о том, как получить коллекцию, на которой построена строка. Здесь будет полезно знать, что с точки зрения логики работы CLR строки — это особый тип данных [3]. При получении указателя на объект строки возвращается указатель не на сам объект System.String, в котором есть поле, в котором находится коллекция Char-ов, а сразу указатель на коллекцию Char-ов.
Это значит, что воспользовавшись инструкцией fixed [4] внутри блока 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-ы [5] — абстракция для типобезопасной работы с последовательным фрагментом управляемой или неуправляемой памяти. Также у строк появился метод расширение .AsSpan(), который позволяет получить коллекцию Char-ов в виде неизменяемого ReadOnlySpan<Char>. Теперь необходимо конвертировать ReadOnlySpan<Char> в Span<Char>. Для этих целей подойдет класс MemoryMarshal [6], в котором есть методы:

  • 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 [7], которая помимо прочего позволяет осуществлять вызов функций в неуправляемых библиотеках из управляемого кода. Это позволяет собрать собственную динамическую библиотеку, реализованную, к примеру, на 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(char* lha);
  • StringMutLib.cpp

    STRINGMUTHLIB_API void MutateSecondCharToV(char* lha)
    {
        char* 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);
}

Подробнее [8] про данный метод, а также о том, как избежать описанных далее побочных эффектов при его использовании, рассказал широкоизвестный в узких кругах Dr. Friedrich von Never [9].

Побочные эффекты

После применения любого из описанных выше способов для изменения строк в программе проявится "побочный эффект": Каждый вызов Console.WriteLine("Test") будет выводить в консоль не Test, а Tvst. Подобный "эффект" связан с механизмом интернирования литеральных строк.
Среда CLR поддерживает хранилище строк в виде таблицы, называемой пулом интернирования. Эта таблица содержит ссылку на каждую уникальную строку литерала, объявленную или созданную в программе.Это позволяет экземпляру литеральной строки с определенным значением встречаться в системе только один раз. Манипуляции по изменению строк из предыдущих разделов создали ситуацию, когда CLR не знает, что указатель, который раньше указывал на строку Test, теперь указывает на Tvst, как если бы в супермаркете кто-то поставил товар под несоответствующий ему ценник. По этой причине не стоит использовать эти методы без особой необходимости.

String.Create

Если необходимо изменять строки во время их создания с целью повышения производительности, то для этих целей подойдет метод [10] 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. Последний из рассмотренных методов, напротив, является вполне законным и уместным способом изменять строки и более того может служить для оптимизации работы приложения.

Автор: Александр Васюков

Источник [11]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/c-2/383079

Ссылки в тексте:

[1] документации Microsoft: https://learn.microsoft.com/ru-ru/dotnet/csharp/programming-guide/strings/

[2] коллекций: https://learn.microsoft.com/ru-ru/dotnet/standard/collections/

[3] особый тип данных: https://mattwarren.org/2016/05/31/Strings-and-the-CLR-a-Special-Relationship/

[4] fixed: https://learn.microsoft.com/ru-ru/dotnet/csharp/language-reference/statements/fixed

[5] Span-ы: https://learn.microsoft.com/en-us/dotnet/api/system.span-1?view=net-6.0

[6] MemoryMarshal: https://learn.microsoft.com/ru-ru/dotnet/api/system.runtime.interopservices.memorymarshal?view=net-7.0

[7] P/Invoke: https://learn.microsoft.com/ru-ru/dotnet/standard/native-interop/pinvoke

[8] Подробнее: https://fornever.me/ru/posts/2017-09-20-clr-string-marshalling.html

[9] Dr. Friedrich von Never: https://fornever.me/ru/

[10] метод: https://learn.microsoft.com/ru-RU/dotnet/api/system.string.create?view=net-7.0#system-string-create-1(system-int32-0-system-buffers-spanaction((system-char-0)))

[11] Источник: https://habr.com/ru/post/718932/?utm_source=habrahabr&utm_medium=rss&utm_campaign=718932