Как стать автором
Обновить

Комментарии 21

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

UPD: пардон, был невнимателен. В последнем примере вы не модифицируете String вообще.

И, главное, зачем эти извращения, если для изменяемых строк специально сделан StringBuilder?

Развлечения ради, и, возможно, более глубокое понимание CLR. Вообще изначально задачу принесли с какого-то собеседования.

Да, понимание – дело хорошее. Особенно хорошо, если понимание доходит до уровня "зачем строки сделаны иммутабельными".

В любой ситуации надо говорить либо "зависит", либо "перфоманс".

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

Первый абзац – блеск). Абсолютно точный ответ на кучу вопросов (в т.ч. и на этот), не дающий никакой информации :-).

Работает до первого резонного "зависит от чего?".

Уже 10 лет как статье, где более подробно описано всё это

Благодаря тому, что строка изменяется до того, как она была проинтернирована, этот способ не вызывает побочных эффектов, свойственных предыдущим методам.

Из этого складывается впячатление, что интернируются все строки, но это не так. Интернируются строки созданные на этапе компиляции, а именно константы. Строка созданная через string.Create string.Join и многими другими способами интернирована не будет, то есть все, что создаются во время работы приложения.
p.s. но если очень нужно есть специальный метод String.Intern

Кстати, если помните стандарт: гарантируется, что строка интернирована не будет, или компилятор/среда имеет право воткнуть String.Intern, где ему нравится? (семантика строк при этом не меняется, так что если такая процедура ведёт к оптимизации – почему бы нет?)

А в целом за статью спасибо, было интересно:)

Да, вы совершенно правы! Мне трудно вспомнить почему я так написал, хотя очевидно что интернирования там не будет, но сейчас поправлю.

var buffer = new char[] { 'T', 'e', 's', 't' };
Span<char> external = Array.Empty<char>();
string result = string.Create(buffer.Length, buffer, (chars, buf) => {
    for (int i = 0; i < chars.Length; i++)
    {
        chars[i] = buf[i];
    }
});



Console.WriteLine(result);
Console.WriteLine(Object.ReferenceEquals(result, "Test")); // False

Вопрос про второй способ (Span), ну, или вообще любой.

Допустим я получил из строки Span, поменял что-то, или вообще хочу взять слайс, потому что мне не нужны последние 2 символа. Если я вызову .ToString(), для того чтобы обратно получить строку, я получу в лоб аллокацию. Выглядит так, что весь смысл string -> span -> string потерялся.

[MethodImpl(MethodImplOptions.InternalCall)]
[DynamicDependency("Ctor(System.ReadOnlySpan{System.Char})")]
public extern String(ReadOnlySpan<char> value);

private static unsafe string Ctor(ReadOnlySpan<char> value)
{
    if (value.Length == 0)
        return Empty;

    string result = FastAllocateString(value.Length);
    Buffer.Memmove(ref result._firstChar, ref MemoryMarshal.GetReference(value), (uint)value.Length);
    return result;
}


Есть какой-то способ избежать аллокации тут, переиспользовав массив из оригинальной строки?
  1. Вам нет нужды вызывать .ToString(), вы же изменяете объект по ссылке без изменения ссылки, если я правильно понял вопрос и если мы все еще говорим о способах, которые крайне не рекомендуется использовать в реальных проектах.

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

Извиняюсь, не ясно спросил.

Я думал про какой-то такой пример:

string test = "Test"; 
Span<char> span = MemoryMarshal.CreateSpan(ref MemoryMarshal.GetReference(test.AsSpan()), test.Length); 
span[1] = span[3]; 
Console.WriteLine(test); // Ttst
        
Span<char> newSpan = span.Slice(0, 2);
Console.WriteLine(newSpan.ToString()); // Tt


То есть, мы получили span, который взят из оригинальной строки. Потом допустим хотим удалить последние 2 символа строки, условно говоря мой newSpan из кода выше. Сам по себе слайс ничего не сделает, просто выставит новые границы. Вот и было интересно, можно ли как-то в оригинальной строке, которую мы поменяли одним из небезопастных способов, потом еще и откусить суффикс, тем самым удалив несколько символов в конце.

Думаю так не выйдет, наверняка не знаю, но как рассуждал:
Скорее всего строки иммутабельны не просто так, значит длина строки, скорее всего, высчитывается при ее создании и больше не актуализируются.
Важно понимать, что Span дает нам доступ к коллекции внутри String, в то время как CLR оперирует классом System.String

Крохотный эксперимент:

using System.Runtime.InteropServices;

var a = "Hello, World!";
Console.WriteLine(a); // Hello, World!
Console.WriteLine(a.Length); // 13
var stringSpan = MemoryMarshal.CreateSpan(ref MemoryMarshal.GetReference(a.AsSpan()), a.Length);
stringSpan[^1] = '\0';
stringSpan[^2] = '\0';
stringSpan[^3] = '\0';
Console.WriteLine(a); // Hello, Wor
Console.WriteLine(a.Length); // 13
Ага, я вот тоже не смог придумать, как поменять размер строки, при условии что нам еще и DWORD m_StringLength надо поменять.

Если же речь о том, чтобы писать в Span, который мы передаем в качестве аргумента, то это плохая идея. Мы не знаем, где в действительности находится память, абстракцию к которой дает span. Она может оказаться как на стеке, так и в неуправляемой среде или внезапно быть частью другой строки.

MutateSecondCharToV() должна же принимать аргумент wchar_t*, а не char* при объявлении CharSet.Unicode?

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

Тут не правильности дело, я пытаюсь себе представить условия, при каких ваш код вообще может работать, и пока не получается. Ну ладно, в dotNet Unicode little endian, при обращении к первому байту двухбайтового символа в случае латинской буквы ничего заметного не произойдет (во втором байте все равно только 0), но у вас же обращение ко второму символу

Ваша правда, забыл dll перебилдить после правок, и полный уверенности, что все в порядке внес код в статью, сейчас исправлю

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории