Magazine - строкові типи в delphi

У цій статті будуть висвітлені наступні питання:

  1. Які строкові типи існують в Delphi, і чим вони відрізняються один від одного
  2. Перетворення рядків з одного типу в інший
  3. Деякі прийоми використання рядків типу AnsiString:
    1. Функції для роботи з рядками про які багато часто забувають або зовсім не знають
    2. Передача рядків у якості параметрів
    3. Використання рядків в записах
    4. Запис в файл і читання з файлу
    5. Використання рядків в якості параметрів і результатів функцій розміщених в DLL.

Ну що, цікаво? Тоді поїхали.

Які строкові типи існують в Delphi, і чим вони відрізняються один від одного?

В Delphi 1.0 існував лише єдиний строковий тип String, повністю еквівалентний однойменним типом в Turbo Pascal і Borland Pascal. Однак, цей тип має істотні обмеження, про які я розповім пізніше. Для обходу цих обмежень, в Delphi 2, розробники з Borland влаштували невелику революцію. Тепер, починаючи з Delphi 2, є три фундаментальних строкових типу: ShortString, AnsiString, і WideString. Крім того, тип String тепер став логічним. Тобто в залежності від настройки відповідного режиму компілятора (режим великих рядків), він прирівнюється або до типу ShortString (для сумісності зі старими програмами), або до типу AnsiString (за замовчуванням). Керувати режимом, можна використовуючи директиву компіляції (коротка форма) або з вікна налаштувань проекту - вкладка "Compiler" -> галочка "Huge strings". Якщо режим включений, то String прирівнюється до AnsiString, інакше String прирівнюється ShortString. З цього правила є виняток: якщо у визначенні типу String зазначений максимальний розмір рядка, наприклад String [25], то, незалежно від режиму компілятора, цей тип буде прирівняний до ShortString відповідного розміру.

Оскільки, як ви дізнаєтеся в подальшому, типи ShortString і AnsiString мають принципову відмінність в реалізації, то я взагалі не рекомендую користуватися логічним типом String без вказівки розміру, якщо Ви, звичайно, не пишете програм під Delphi 1. Якщо ж Ви все-таки використовуєте тип String, то я настійно рекомендую прямо в коді Вашого модуля вказувати директиву компіляції, яка встановлює Автоматичне виведення Вами режим роботи компілятора. Особливо якщо Ви використовуєте особливості реалізації відповідного строкового типу. Якщо цього не зробити, то одного разу, коли Ваш код потрапить в руки іншого програміста, не буде ніякої гарантії того, що його компілятор буде налаштований, так само як і Ваш.

Оскільки за замовчуванням, після установки Delphi, режим великих рядків увімкнено, більшість молодих програмістів навіть і не підозрюють, що String може представляти щось відмінне від AnsiString. Тому, далі в цій статті, будь-яка згадка типу String без вказівки розміру, має на увазі, що він дорівнює типу AnsiString, якщо не буде явно зазначено інше. Тобто вважається що настройка компілятора відповідає настройці за замовчуванням.

Відразу ж згадаю про відмінності між типами AnsiString і WideString. Ці типи мають практично однакову реалізацію, і відрізняються лише тим, що WideString використовується для подання рядків в кодуванні UNICODE використовує 16-ти бітове представлення кожного символу (WideChar). Ця кодування використовується в тих випадках коли необхідна можливість одночасної присутності в одному рядку символів з двох і більше мов (крім англійської). Наприклад, рядків містять одночасно символи англійського, українського і європейських мов. За цю можливість доводиться платити - розмір пам'яті, займаний такими рядками в два рази більше розміру, займаного звичайними рядками. Використання WideString зустрічається не часто, тому, я буду в основному розповідати про рядках типу AnsiString. Але, оскільки вони мають однакову реалізацію, майже все сказане щодо AnsiString буде дійсно і для WideString, природно з урахуванням різниці в розмірі кожного символу.

Теж саме стосується і різниці між pChar і pWideChar.

Строковий тип AnsiString, зазвичай використовується для подання рядків в кодуванні ANSI, або інших (наприклад OEM) в яких для кодування одного символу використовується один байт (8 біт). Такий спосіб кодування називається single-byte character set, або SBCS. Але, дуже багато хто не знає про існування ще одного способу кодування багатомовних рядків (крім UNICODE) використовуваного в системах Windows і Linux. Цей спосіб називається multibyte character sets, або MBCS. При цьому способі, деякі символи представляються одним байтом, а деякі, двома і більше. На відміну від UNICODE, рядки, закодовані таким способом, вимагають менше пам'яті для свого зберігання, але вимагають більш складної обробки. Так ось, строковий тип AnsiString може використовуватися для зберігання таких рядків. Я не буду детально зупинятися на цьому способі кодування, оскільки він застосовується вкрай рідко. Особисто я, ні разу не зустрічав програм використовують даний спосіб кодування.

Знавці Delphi ймовірно мені відразу нагадають ще й про типи pChar (pWideChar) і array [. ] Of Char. Однак, я вважаю, що це не зовсім рядкові типи, але я розповім і про них, оскільки вони дуже часто використовуються в поєднанні із строковими типами.

Отже, наведу основні характеристики строкових типів:

Найбільшою довжиною строки

Обсяг пам'яті, необхідний для зберігання рядка

String [n] де 0 0, тому то Delphi і не звільняє пам'ять, зайняту рядком.

Ще, цей лічильник використовується і для вирішення проблем, пов'язаних з такою ситуацією:

При роботі з рядками певними як константи, алгоритм роботи дещо відрізняється. Наведу приклад:

Здавалося б, при завершенні роботи процедури, примірник рядки 'Вася' повинен бути знищений. Але в даному випадку це не так. Адже, при наступному вході в процедуру, для виконання присвоювання потрібно буде знову десь взяти рядок 'Вася'. Для цього, ще при компіляції, Delphi розміщує екземпляр рядка 'Вася' в області констант програми, де її навіть неможливо змінити, принаймні, простими методами. Але як же при завершенні процедури визначити що рядок 'Вася' - константная рядок, і її не можна знищувати? Все дуже просто. Для константних рядків, лічильник посилань встановлюється рівним -1. Це значення, "вимикає" нормальний алгоритм роботи зі "лічильником посилань". Він не збільшується при присвоєнні, і не зменшується при знищенні змінної. Однак, при спробі зміни змінної (пам'ятаєте s2 [1]: = 'X'), значення лічильника дорівнює -1 буде завжди вважатися ознакою того, що на рядок посилається більше однієї змінної (адже він не дорівнює 1). Тому, в такій ситуації завжди буде створюватися унікальний екземпляр рядка, природно, без декремента лічильника посилань старої. Це захистить від змін екземпляр рядка-константи.

На жаль, цей алгоритм спрацьовує не завжди. Але про це, ми поговоримо пізніше, при розгляді питань перетворення строкових типів.

Де ж Delphi зберігає "лічильник посилань"? Причому, для кожного рядка свій! Природно, разом із самою рядком. Ось що являє собою ця область пам'яті, що зберігає екземпляр рядка 'abc':

Лічильник посилань рівний -1

З полем зі зміщення -8, нам уже має бути все ясно. Це значення, яке зберігається в подвійному слові (4 байта), той самий лічильник, який дозволяє оптимізувати зберігання однакових рядків. Значення цього лічильника має тип Integer, тобто може бути негативним. Насправді, використовується лише одне негативне значення - "-1", і позитивні значення. 0 не використовується.

Тепер, зверніть увагу на поле, яке лежить за зміщення -4. Це, четирёхбайтовое значення довжини рядка (майже як в ShortString). Думаю, Ви помітили, що розмір пам'яті виділеної під цей рядок не має надмірності. Тобто компілятор виділяє під рядок мінімально необхідну кількість байт пам'яті. Це звичайно добре, але, при спробі "наростити" рядок: s1: = s1 + 'd', компілятору, точніше бібліотеці часу виконання (RTL) доведеться перерозподілити пам'ять. Адже тепер рядку потрібно більше пам'яті, аж на цілий байт. Для перерозподілу пам'яті потрібно знати поточний розмір рядка. Ймовірно, саме для того, що б наступного разу галерея сканувати рядок, визначаючи її розмір, розробники Delphi і включили поле довжини, рядки в цю структуру. Довжина рядка, зберігається як значення Integer, звідси і обмеження на максимальний розмір таких рядків - 2 Гбайт. Сподіваюся, ми не скоро упрёмся в це обмеження. До речі, саме тому, що пам'ять під ці рядки виділяється динамічно, вони і отримали ще одне свою назву: динамічні рядки.

Залишилося розповісти ще про декілька особливості змінних AnsiString. Найважливішою особливістю значень цього типу є можливість приведення їх до типу Pointer. Це втім, природно, адже в "душі" вони і є покажчики, як би вони цього не приховували. Наприклад, якщо описані змінні: s: AnsiString і p: Pointer. Те виконання оператора p: = Pointer (s) призведе до того, що змінна p стане вказувати на екземпляр рядка. Однак, при цьому, дуже важливо знати: лічильник посилань цього рядка не буде збільшено. Але про це, ми поговоримо трохи пізніше.

Оскільки, змінні цього типу реально є покажчиками, то для них і реально таке значення як Nil - покажчик в "нікуди". Це значення у змінній типу AnsiString за змістом прирівнюється порожній рядку. Більш того, щоб не витрачати пам'ять і час на ведення лічильника посилань, і поля розміру рядка завжди рівного 0, при присвоєнні порожній рядку змінної цього типу, реально, присвоюється значення Nil. Це не очевидно, оскільки зазвичай не помітно, але як ми побачимо пізніше, дуже важлива особливість.

Ось тепер, здається, Ви знаєте про рядках все. Настала пора переходити до більш цікавої частини статті - як з цим всім жити?

Перетворення рядків з одного типу в інший

Тут, все як завжди, і просто і складно.

Перетворення між "справжніми" строковими типами String [n], ShortString, і AnsiString виконуються легко, і прозоро. Ніяких явних дій робити не треба, Delphi все зробить за Вас. Треба лише розуміти, що в маленьке велике не влазить. наприклад:

В результаті виконання цього коду, в змінної s3 виявиться рядок 'abc', а не 'abcdef'. З перетворенням з pChar в String [n], ShortString, і AnsiString, теж все дуже не погано. Просто присвоюйте, і все буде нормально.

Складнощі починаються тоді, коли ми починаємо перетворювати "справжні" рядкові типи в pChar. Безпосереднє присвоювання змінним типу pChar значень рядків не допускається компілятором. На оператор p: = s де p має тип pChar, а s: AnsiString, компілятор видасть повідомлення: "Incompatible types: 'String' and 'PChar'" - несумісні типи 'String' і 'PChar'. Щоб уникнути такої помилки, треба застосовувати явне приведення типу: p: = pChar (s). Так рекомендують розробники Delphi. Загалом, вони мають рацію. Але, якщо згадати, як зберігаються динамічні рядки - з нулем в кінці, як і pChar. А ще й те, що до AnsiString можна застосувати перетворення в тип Pointer. Чи стане очевидним, що за все, можливо цілих три способи перетворення рядка в pChar:

Всі вони, синтаксично правильні. І здається, що все три покажчика (p1, p2 і p3) будуть в результаті мати одне і те ж значення. Але це не так. Все залежить від того, що знаходиться в s. Якщо бути більш точним, так само чи значення s порожній рядку, чи ні:

Щоб Ви розуміли причину такого явища, я опишу, як Delphi виконує кожне з цих перетворень. На початку нагадаю, що змінні AnsiString представляють порожні рядки, реально мають значення Nil. Так ось:

Ви зможете конвертувати pChar (s), компілятор генерує виклик спеціальної внутрішньої функції @LstrToPChar. Ця функція перевіряє - якщо строкова змінна має значення Nil, то замість нього, вона повертає покажчик на реально розміщену в пам'яті порожній рядок. Тобто pChar (s) ніколи не поверне покажчик рівний Nil.

викликає помилку доступу до пам'яті при виконанні рядки з позначкою 1. Ось і доводиться застосовувати цю функцію.

Тут, оскільки параметр передається за значенням, при виклику буде створена локальна копія змінної (покажчика) Msg. Про це я ще розповім нижче. Ця змінна, при завершенні процедури буде знищуватися, що призведе до звільнення "персональної" копії примірника переданої і перетвореної рядки.

Функції цієї групи використовуються для порівняння рядків. Результатом буде одне із значень:

> 0 якщо S1> S2
255 символів доводиться платити.

Якщо ж тобі досить і 255 символів, то використовуй ShortString, або String [n].

Використання рядків в записах

Проблема виникає тоді, коли одне поле (або кілька полів) мають тип динамічної рядки. В цьому випадку, часто виникає проблема схожа з проблемою записи динамічної рядки в файл - не всі дані лежать в запису, динамічні рядки представлені в ній лише покажчиками. Вирішується проблема також як і з записом рядків в файл. Шкода тільки що тоді не можна буде оперувати (записати / прочитати) цілком всієї записом. Рядки доведеться обробляти окремо. Можна зробити, наприклад, так:

Використання рядків в якості параметрів і результатів функцій розміщених в DLL.

Загальний сенс цього епосу в тому, що якщо Ваша Dll експортує хоча б одну процедуру або функцію з типом параметра відповідним будь динамічної рядку (AnsiString наприклад), або функцію, яка повертає результат такого типу. Ви повинні обов'язково і в Dll, і в що використовує її програмі, першим модулем в списку імпорту (uses) вказати модуль ShareMem. І як наслідок, постачати зі своєю програмою і Dll ще одну стандартну бібліотеку BORLNDMM.DLL.

Ви не замислювалися над питаннями: "Навіщо всі ці складності?"; "Що буде якщо цього не зробити?" і "Чи можна цього уникнути?"; "Якщо так, то як?" Якщо не замислювалися, то саме час зробити це.

Спробуємо розібратися що буде відбуватися з екземплярами динамічних рядків в наступному прикладі:

Все начебто добре. Але до тих пір, поки обидві процедури розташовуються в одному виконуваному модулі (EXE-файлі). Якщо наприклад помістити процедуру Y в Dll, а процедуру X залишити в EXE, то буде біда.

Справа в тому, що виділенням і звільненням пам'яті для примірників динамічних рядків займається внутрішній менеджер пам'яті Delphi-додатки. Використовувати стандартний менеджер Windows дуже накладно. Він занадто універсальний, і тому повільний, а рядки дуже часто вимагають перерозподілу пам'яті. Ось розробники Delphi і створили свій. Він веде списки розподіленої і вільної пам'яті свого застосування. Так ось, вся біда в тому, що Dll буде використовуватися свій менеджер пам'яті, а EXE свій. Один про одного вони нічого не знають. Тому, спроба звільнення блоку пам'яті виділеного не своїм менеджером призведе до серйозного порушення в його роботі. Причому, це порушення може проявитися далеко не відразу, і досить незвичайним чином.

У нашому випадку, пам'ять під рядок '100' буде виділена менеджером EXE-файлу, а звільнятися вона буде менеджером DLL. Те ж відбудеться і з пам'яттю під рядок '100 $', тільки навпаки.

Для подолання цієї проблеми, розробники Delphi створили бібліотеку BORLNDMM.DLL. Вона включає в себе ще один менеджер пам'яті :). Використання ж модуля ShareMem, призводить до того, що він замінює вбудований в EXE (DLL) менеджер пам'яті на менеджер розташований в BORLNDMM.DLL. Тобто тепер і EXE-файл і DLL, будуть використовувати один, загальний менеджер пам'яті.

Тут важливо відзначити те, що якщо який-небудь з програмних модулів (EXE або DLL) не матимуть в списку імпорту модуля ShareMem, то вся робота піде нанівець. Знову будуть працювати кілька менеджерів пам'яті. Знову буде бардак.

Ну ось, нарешті і все. Тепер, Ви знаєте про рядках майже стільки ж як я :).

Звичайно, цим тема не вичерпується. Наприклад, я нічого не розповів про мультібайтових рядках (MBCS) використовуваних для багатомовними додатків. Може і ще щось забув розповісти. Але, не турбуйтеся. Я свої знання отримував, вивчаючи книги, тексти своїх і чужих програм, код згенерований компілятором, і т.п. Тобто все з відкритих джерел. Значить це все доступно і Вам. Головне, щоб Ви були допитливими, і частіше задавали собі питання "Як?", "Чому?", "Навіщо?". Тоді у всьому зможете розібратися і самі.