Зважаючи на те, що рядки незмінні в .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) час.
Як ви думаєте , ви б скопіювати купу байт в постійна час?
EDIT: Мехрдад пропонує взагалі не копіювати рядок, а зберігати посилання на фрагмент.
Розглянемо в .Net - багатомегабайтну рядок, на який хтось дзвонить .SubString(n, n+3)
(для будь-якого n посеред рядка).
Тепер рядок ENTIRE не може бути зібраний сміттям лише тому, що одна посилання містить 4 символи? Це здається смішним марнотратством.
Крім того, відстеження посилань на підрядки (які можуть бути навіть всередині підрядків) та намагання копіювати в оптимальний час, щоб уникнути поразки GC (як описано вище), робить концепцію кошмаром. Набагато простіше та надійніше скопіювати .SubString
та підтримувати просту незмінну модель.
EDIT: Ось трохи прочитайте про небезпеку збереження посилань на підрядки у більших рядках.
memcpy
що все ще є O (n).
char*
підрядку.
Java (на відміну від .NET) пропонує два способи роботи Substring()
, ви можете розглянути, чи хочете ви зберегти лише посилання або скопіювати цілу підрядку в нове місце пам'яті.
Простий .substring(...)
ділиться внутрішньо використовуваним char
масивом з оригінальним об'єктом String, який ви потім new String(...)
зможете скопіювати у новий масив, якщо це потрібно (щоб уникнути перешкод для збирання сміття оригінального).
Я думаю, що така гнучкість - найкращий варіант для розробника.
.substring(...)
.
Java використовується для посилання на більші рядки, але:
Я відчуваю, що це можна покращити: чому б просто не зробити копіювання умовно?
Якщо підрядок становить щонайменше половину розміру з батьківського, можна посилатися на батьківського. Інакше можна просто зробити копію. Це дозволяє уникнути витоку багато пам’яті, при цьому все ще надаючи значну користь.
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*
a- fixed
Statement, вказівник буде вказувати на 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, мабуть, кращий. Якщо вам потрібно зберегти це деякий час, і ви збираєтесь зберегти лише невеликий відсоток від початкового рядка, зробити належну підрядку (щоб обрізати зайві дані), мабуть, краще. Точка переходу десь посередині, але це залежить від конкретного використання.