Якщо рядки незмінні в .NET, то чому для Substring потрібно O (n) час?


451

Зважаючи на те, що рядки незмінні в .NET, мені цікаво, чому вони були розроблені таким чином, щоб замість них string.Substring()зайняти час O ( substring.Length) O(1)?

тобто які були компроміси, якщо такі були?


3
@Mehrdad: Мені подобається це питання. Скажіть, будь ласка, як ми можемо визначити O () заданої функції в .Net? Це зрозуміло чи ми повинні його обчислити? Дякую
odiseh

1
@odiseh: Іноді (як у цьому випадку) зрозуміло, що рядок копіюється. Якщо це не так, ви можете або переглянути документацію, виконати орієнтири, або спробувати заглянути в вихідний код .NET Framework, щоб зрозуміти, що це таке.
користувач541686

Відповіді:


423

ОНОВЛЕННЯ: Це питання мені дуже сподобалось, я просто його провів. Див. Рядки, незмінність та наполегливість


Коротка відповідь: O (n) - O (1), якщо n не зростає великим. Більшість людей витягують крихітні підряди з крихітних рядків, тож як асимптотично зростає складність, абсолютно не має значення .

Довга відповідь:

Незмінна структура даних, побудована таким чином, що операції над екземпляром дозволяють повторно використовувати пам'ять оригіналу лише з невеликою кількістю (зазвичай O (1) або O (lg n)) копіювання або нового розподілу, називається "стійкою" незмінна структура даних. Рядки в .NET незмінні; ваше питання по суті "чому вони не наполегливі"?

Тому що, дивлячись на операції, які, як правило, виконуються на рядках в .NET-програмах, навряд чи гірше взагалі просто зробити абсолютно новий рядок.Витрати та труднощі створення складної стійкої структури даних не окупаються.

Люди зазвичай використовують "підрядку", щоб витягти короткий рядок - скажімо, десять-двадцять символів - із дещо довшого рядка - можливо, пару сотень символів. У вас є рядок тексту у файлі, розділеному комою, і ви хочете витягнути третє поле, яке є прізвищем. У рядку буде, можливо, кілька сотень символів, назва - пару десятків. Розподіл рядків і копіювання пам'яті в п'ятдесят байтів надзвичайно швидко на сучасному обладнанні. Дивно, що створення нової структури даних, яка складається з вказівника на середину існуючого рядка плюс довжини, також вражаюче швидко, не має значення; "досить швидкий", за визначенням досить швидкий.

Видобуті підряди, як правило, мають невеликі розміри та короткий час життя; збирач сміття незабаром поверне їх, і вони взагалі не займають багато місця на купі. Тож використання стійкої стратегії, яка заохочує повторне використання більшої частини пам’яті, також не є виграшною; все, що ви зробили, це змусити ваш сміттєзбірник повільніше, оскільки тепер йому доводиться турбуватися щодо обробки внутрішніх покажчиків.

Якщо операції з підрядкою, які люди зазвичай робили на рядках, були зовсім іншими, то було б доцільно йти зі стійким підходом. Якщо люди зазвичай мали рядки з мільйонними символами і витягували тисячі підкладок, що перекриваються, розмірами в діапазоні сотень тисяч символів, а ці підрядки довго жили в купі, тоді було б доцільним сенсом перейти до стійкої підрядки підхід; було б марно і нерозумно цього не робити. Але більшість бізнес-програмістів не роблять нічого навіть розпливчасто, як подібні речі. .NET - це не платформа, яка призначена для потреб проекту геному людини; Програмісти аналізу ДНК повинні щодня вирішувати проблеми з тими характеристиками використання рядків; шанси хороші, що ви цього не робите. Мало хто з них будує власні стійкі структури даних, які тісно відповідають їхнім сценаріям використання.

Наприклад, моя команда пише програми, які роблять під час введення код C # і VB-коду під час введення. Деякі з цих файлів коду є величезними, і тому ми не можемо робити маніпуляції з рядком O (n) для вилучення підрядів або вставки або видалення символів. Ми створили купу стійких незмінних структур даних для представлення змін у текстовому буфері, які дозволяють швидко та ефективно повторно використовувати основну частину існуючих рядкових даних та існуючі лексичні та синтаксичні аналізи при типовому редагуванні. Цю проблему було важко вирішити, і її рішення було вузько адаптовано до конкретного домену редагування коду C # та VB. Було б нереально сподіватися, що вбудований тип рядка вирішить цю проблему для нас.


47
Було б цікаво порівняти те, як робить Java (або, принаймні, в якийсь момент минулого) це: Substring повертає нову рядок, але вказує на той самий char [], що і більший рядок - це означає, що більший char [] більше не можна збирати сміття, поки підрядок не вийде із сфери застосування. Я віддаю перевагу реалізації .net далеко.
Michael Stum

13
Я подібного коду бачив зовсім небагато: string contents = File.ReadAllText(filename); foreach (string line in content.Split("\n")) ...або інші його версії. Я маю на увазі прочитати цілий файл, а потім обробити різні частини. Цей вид коду був би значно швидшим і вимагав би менше пам’яті, якщо рядок був стійким; ви завжди мали б точно одну копію файлу в пам'яті замість того, щоб копіювати кожен рядок, а потім частини кожного рядка як обробляєте його. Однак, як сказав Ерік, - це не типовий випадок використання.
конфігуратор

18
@configurator: Також у .NET 4 метод File.ReadLines розбиває текстовий файл на рядки для вас, не спочатку читаючи це все в пам'яті.
Ерік Ліпперт

8
@Michael: Java Stringреалізована як стійка структура даних (це не визначено в стандартах, але всі реалізації, які я знаю, роблять це).
Йоахім Зауер

33
Коротка відповідь: Копія даних робиться для того, щоб дозволити збирання сміття вихідного рядка .
Qtax

121

Саме тому, що рядки незмінні, .Substringповинні зробити копію принаймні частини початкового рядка. Створення копії n байтів повинно зайняти O (n) час.

Як ви думаєте , ви б скопіювати купу байт в постійна час?


EDIT: Мехрдад пропонує взагалі не копіювати рядок, а зберігати посилання на фрагмент.

Розглянемо в .Net - багатомегабайтну рядок, на який хтось дзвонить .SubString(n, n+3)(для будь-якого n посеред рядка).

Тепер рядок ENTIRE не може бути зібраний сміттям лише тому, що одна посилання містить 4 символи? Це здається смішним марнотратством.

Крім того, відстеження посилань на підрядки (які можуть бути навіть всередині підрядків) та намагання копіювати в оптимальний час, щоб уникнути поразки GC (як описано вище), робить концепцію кошмаром. Набагато простіше та надійніше скопіювати .SubStringта підтримувати просту незмінну модель.


EDIT: Ось трохи прочитайте про небезпеку збереження посилань на підрядки у більших рядках.


5
+1: Саме мої думки. Внутрішньо він, ймовірно, використовує те, memcpyщо все ще є O (n).
леппі

7
@abelenky: Я думаю, може, не копіюючи його взагалі? Це вже є, навіщо вам це копіювати?
користувач541686

2
@Mehrdad: ЯКЩО ви після виступу. Просто в цьому випадку залишайтеся небезпечними. Тоді ви можете отримати char*підрядку.
леппі

9
@Mehrdad - ви, можливо, там очікували занадто багато, це називається StringBuilder , і це добре будує рядки. Це не називається StringMultiPurposeManipulator
MattDavey

3
@SamuelNeff, @Mehrdad: Рядки в .NET не NULL припиняються. Як пояснено у публікації Ліпперта , перші 4 байти містять довжину рядка. Ось чому, як вказує Скіт, вони можуть містити \0символи.
Елідеб

33

Java (на відміну від .NET) пропонує два способи роботи Substring(), ви можете розглянути, чи хочете ви зберегти лише посилання або скопіювати цілу підрядку в нове місце пам'яті.

Простий .substring(...)ділиться внутрішньо використовуваним charмасивом з оригінальним об'єктом String, який ви потім new String(...)зможете скопіювати у новий масив, якщо це потрібно (щоб уникнути перешкод для збирання сміття оригінального).

Я думаю, що така гнучкість - найкращий варіант для розробника.


50
Ви називаєте це "гнучкість" Я називаю це "Спосіб випадково вставити важку діагностику помилки (або проблеми з продуктивністю) в програмне забезпечення, тому що я не усвідомлював, що я повинен зупинитися і подумати про всі місця, які цей код може бути зателефонували з (включаючи тих, які були б винайдені лише в наступній версії) просто для отримання 4 символів із середини рядка "
Nir

3
downvote втягнуто ... Після трохи ретельнішого перегляду коду це виглядає як підряд у java посилається на спільний масив, принаймні у версії openjdk. І якщо ви хочете забезпечити нову рядок, є спосіб зробити це.
Дон Робі

11
@Nir: Я називаю це "зміщення статусного кво". Для вас спосіб виконання Java здається загроженим ризиками, а вибір .Net - єдиним розумним вибором. Для програмістів Java - це навпаки.
Майкл Боргвардт

7
Я дуже віддаю перевагу .NET, але це звучить як одне, що Java отримала правильно. Корисно, щоб розробнику було дозволено мати доступ до справді O (1) методу підрядки (без прокатки власного типу рядка, який би перешкоджав взаємодії з усіма іншими бібліотеками, і не був би таким ефективним, як вбудоване рішення ). Рішення Java, мабуть, малоефективне (вимагає щонайменше двох купових об’єктів, одного для оригінального рядка та іншого для підрядка); мови, що підтримують фрагменти, ефективно замінюють другий об'єкт парою покажчиків на стеку.
Qwertie

10
Оскільки JDK 7u6 вже не відповідає дійсності - тепер Java завжди копіює вміст рядка для кожного .substring(...).
Xaerxess

12

Java використовується для посилання на більші рядки, але:

Java змінила свою поведінку і на копіювання , щоб уникнути протікання пам'яті.

Я відчуваю, що це можна покращити: чому б просто не зробити копіювання умовно?

Якщо підрядок становить щонайменше половину розміру з батьківського, можна посилатися на батьківського. Інакше можна просто зробити копію. Це дозволяє уникнути витоку багато пам’яті, при цьому все ще надаючи значну користь.


Завжди копіювання дозволяє видалити внутрішній масив. Половина кількості виділень купи, що зберігає пам'ять у звичайному випадку коротких рядків. Це також означає, що вам не потрібно стрибати через додатковий непрямий доступ для кожного символу.
CodesInChaos

2
Я думаю, що важливо взяти з цього те, що Java насправді змінилася від використання тієї ж бази char[](з різними вказівниками до початку і в кінці) до створення нової String. Це чітко показує, що аналіз витрат і вигод повинен показувати перевагу створення нового String.
Філогенез

2

Жоден з відповідей тут не стосувався "проблеми брекетінгу", тобто, що рядки в .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*a- fixedStatement, вказівник буде вказувати на 0x48.)

Ця структура дозволяє швидко шукати довжину рядка (корисно у багатьох контекстах) та дозволяє передавати вказівник в API P / Invoke to 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, мабуть, кращий. Якщо вам потрібно зберегти це деякий час, і ви збираєтесь зберегти лише невеликий відсоток від початкового рядка, зробити належну підрядку (щоб обрізати зайві дані), мабуть, краще. Точка переходу десь посередині, але це залежить від конкретного використання.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.