Учитывая, что строки являются неизменяемыми в .NET, мне интересно, почему они были разработаны таким образом, что вместо string.Substring()
O? substring.Length
) Требуется время O(1)
?
т.е. каковы были компромиссы, если таковые имеются?
Учитывая, что строки являются неизменяемыми в .NET, мне интересно, почему они были разработаны таким образом, что вместо string.Substring()
O? substring.Length
) Требуется время O(1)
?
т.е. каковы были компромиссы, если таковые имеются?
Ответы:
ОБНОВЛЕНИЕ: мне очень понравился этот вопрос, я просто написал в блоге. См Строки, неизменность и постоянство
Короткий ответ: O (n) равно O (1), если n не становится большим. Большинство людей извлекают крошечные подстроки из крошечных строк, поэтому то, как сложность асимптотически возрастает, совершенно не имеет значения .
Длинный ответ:
Неизменяемая структура данных, построенная так, что операции с экземпляром позволяют повторно использовать память оригинала с небольшим объемом (обычно O (1) или O (lg n)) копирования или нового выделения, называется «постоянным» неизменяемая структура данных. Строки в .NET являются неизменяемыми; Ваш вопрос по сути "почему они не являются постоянными"?
Потому что, когда вы смотрите на операции, которые обычно выполняются над строками в программах .NET, во всех соответствующих случаях едва ли вообще хуже просто создать совершенно новую строку. Стоимость и сложность построения сложной постоянной структуры данных не окупаются.
Люди обычно используют «подстроку» для извлечения короткой строки - скажем, десяти или двадцати символов - из несколько более длинной строки - возможно, из пары сотен символов. У вас есть строка текста в файле, разделенном запятыми, и вы хотите извлечь третье поле, которое является фамилией. Длина строки может составить пару сотен символов, а название - пару десятков. Распределение строк и копирование памяти из пятидесяти байтов удивительно быстро на современном оборудовании. То, что создание новой структуры данных, состоящей из указателя на середину существующей строки и длины, также удивительно быстро, не имеет значения; «достаточно быстро» по определению достаточно быстро.
Извлекаемые подстроки, как правило, имеют небольшой размер и короткий срок службы; сборщик мусора скоро вернет их, и они не заняли много места в куче. Поэтому использование устойчивой стратегии, которая поощряет повторное использование большей части памяти, также не является победой; все, что вы сделали, - замедлили сборщик мусора, потому что теперь он должен беспокоиться о работе с внутренними указателями.
Если бы операции с подстрокой, которые люди обычно выполняли со строками, были совершенно другими, то имело бы смысл придерживаться постоянного подхода. Если бы у людей обычно были строки из миллионов символов, и они извлекали тысячи перекрывающихся подстрок с размерами в диапазоне сотен тысяч символов, и эти подстроки долгое время жили в куче, тогда было бы разумно использовать постоянную подстроку. подходить; это было бы расточительно и глупо не делать этого. Но большинство программистов, занимающихся бизнесом, не делают ничего, даже смутно подобного, .NET не является платформой, адаптированной для нужд проекта «Геном человека»; Программисты анализа ДНК должны решать проблемы с этими характеристиками использования строк каждый день; хорошие шансы, что вы нет. Те немногие, кто создает свои собственные постоянные структуры данных, точно соответствуют сценариям их использования.
Например, моя команда пишет программы, которые на ходу анализируют код C # и VB по мере его ввода. Некоторые из этих файлов кода огромны, и поэтому мы не можем делать O (n) -текстовые манипуляции для извлечения подстрок или вставки или удаления символов. Мы создали множество постоянных неизменяемых структур данных для представления изменений в текстовом буфере, что позволяет нам быстро и эффективно повторно использовать большую часть существующих строковых данных и существующих лексических и синтаксических анализов при типичном редактировании. Это была трудная проблема, и ее решение было узко приспособлено для конкретной области редактирования кода на C # и VB. Было бы нереально ожидать, что встроенный строковый тип решит эту проблему для нас.
string contents = File.ReadAllText(filename); foreach (string line in content.Split("\n")) ...
или другие его версии. Я имею в виду прочитать весь файл, а затем обработать различные части. Код такого рода будет значительно быстрее и потребует меньше памяти, если строка будет постоянной; у вас всегда будет ровно одна копия файла в памяти вместо того, чтобы копировать каждую строку, а затем части каждой строки в процессе ее обработки. Однако, как сказал Эрик, это не типичный вариант использования.
String
реализован как постоянная структура данных (это не указано в стандартах, но все известные мне реализации делают это).
Именно потому, что строки являются неизменяемыми, .Substring
необходимо сделать копию хотя бы части исходной строки. Создание копии из n байтов должно занять O (n) времени.
Как вы думаете, вы бы скопировали кучу байтов в постоянное время?
РЕДАКТИРОВАТЬ: Mehrdad предлагает вообще не копировать строку, но сохранить ссылку на ее часть.
Рассмотрим в .Net строку размером в несколько мегабайт, по которой кто-то звонит .SubString(n, n+3)
(для любого n в середине строки).
Теперь ВСЮ строку нельзя собирать мусором только потому, что одна ссылка содержит до 4 символов? Это кажется нелепой тратой пространства.
Кроме того, отслеживание ссылок на подстроки (которые могут даже находиться внутри подстрок) и попытка копирования в оптимальные моменты времени, чтобы избежать победы над GC (как описано выше), делают эту концепцию кошмаром. Гораздо проще и надежнее копировать .SubString
и поддерживать прямую неизменную модель.
РЕДАКТИРОВАТЬ: Вот хорошее небольшое чтение об опасности сохранения ссылок на подстроки в более крупных строках.
memcpy
который все еще O (n).
char*
подстроку.
NULL
заканчиваются. Как объясняется в посте Липперта , первые 4 байта содержат длину строки. Вот почему, как указывает Скит, они могут содержать \0
символы.
Java (в отличие от .NET) предоставляет два способа работы Substring()
: вы можете решить, хотите ли вы сохранить только ссылку или скопировать целую подстроку в новое место в памяти.
Простой .substring(...)
разделяет используемый внутри char
массив с исходным объектом String, который затем new String(...)
можно при необходимости скопировать в новый массив (чтобы не мешать сборке мусора исходного).
Я думаю, что такая гибкость - лучший вариант для разработчика.
.substring(...)
.
Ява использовалась для ссылки на более крупные строки, но:
Я чувствую, что это можно улучшить, хотя: почему бы просто не сделать условное копирование?
Если подстрока по крайней мере вдвое меньше родительского, можно ссылаться на родительский. В противном случае можно просто сделать копию. Это позволяет избежать утечки большого количества памяти, но при этом обеспечивает значительную выгоду.
char[]
(с разными указателями на начало и конец) на создание новой String
. Это ясно показывает, что анализ затрат и выгод должен показывать предпочтение созданию нового String
.
Ни один из приведенных здесь ответов не относится к «проблеме скобок», то есть строки в .NET представлены в виде комбинации BStr (длина, хранящаяся в памяти «до» указателя) и CStr (строка заканчивается на '\ 0').
Строка "Hello there", таким образом, представляется как
0B 00 00 00 48 00 65 00 6C 00 6F 00 20 00 74 00 68 00 65 00 72 00 65 00 00 00
(если он назначен char*
в fixed
-состоянии, указатель будет указывать на 0x48.)
Эта структура обеспечивает быстрый поиск длины строки (полезно во многих контекстах) и позволяет передавать указатель в API P / Invoke для Win32 (или других), которые ожидают строку с нулевым символом в конце.
Когда вы выполняете Substring(0, 5)
правило «о, но я обещал, что после последнего символа будет нулевой символ», вам нужно сделать копию. Даже если вы получили подстроку в конце, тогда не было бы места для длины без искажения других переменных.
Однако иногда вы действительно хотите поговорить о «середине строки», и вам не обязательно заботиться о поведении P / Invoke. Недавно добавленная ReadOnlySpan<T>
структура может быть использована для получения подстроки без копирования:
string s = "Hello there";
ReadOnlySpan<char> hello = s.AsSpan(0, 5);
ReadOnlySpan<char> ell = hello.Slice(1, 3);
ReadOnlySpan<char>
«Подстрока» сохраняет длину независимо друг от друга, и это не гарантия того, что есть «\ 0» после конца значения. Он может быть использован во многих отношениях «как строка», но это не «строка», поскольку он не имеет характеристик BStr или CStr (тем более, что они оба). Если вы никогда (напрямую) не вызываете P / Invoke, то нет особой разницы (если только API, который вы хотите вызвать, не ReadOnlySpan<char>
перегружен).
ReadOnlySpan<char>
не может использоваться в качестве поля ссылочного типа, поэтому есть также ReadOnlyMemory<char>
( s.AsMemory(0, 5)
), который является косвенным способом иметь ReadOnlySpan<char>
, поэтому такие же отличия от string
существующих.
В некоторых ответах / комментариях к предыдущим ответам говорилось о расточительности, когда сборщик мусора должен хранить строку из миллиона символов, пока вы продолжаете говорить о 5 символах. Именно такое поведение вы можете получить при ReadOnlySpan<char>
подходе. Если вы просто делаете короткие вычисления, подход ReadOnlySpan, вероятно, лучше. Если вам нужно сохранить его на некоторое время, и вы собираетесь сохранить только небольшой процент от исходной строки, возможно, лучше сделать правильную подстроку (чтобы обрезать лишние данные). Где-то посередине есть точка перехода, но это зависит от вашего конкретного использования.