Публикации FreePascal

Символы и строки в Unicode-версиях Free Pascal

18.07.2012
Лебедев С.В.

Для начала сразу - к чему это относится, чтобы не было лишних иллюзий:

  • FPC 2.7.1 или выше
  • Lazarus, если где упоминается, 1.1

Для проверки идей и кода использовалась сборка CodeTyphon 2.70

Да, именно она, несмотря на выраженное общественное "фи" относительно "нормальности" этого проекта и всю его глючь, являющуюся результатом работы на модифицированных trunc-версиях fpc/lazarus, выбранных достаточно странным образом; причем за некоторые модификации, типа принудительного размещения окон среды разработки, фиксированно вписывающихся только в мониторы высотой не менее 1024 пикселов и неотключаемый криво работающий docking, а также вымаранный диалог установки позиций и размеров этих самых окон, можно было бы сказать пару нехороших слов авторам, но "наши греческие друзья" немногословны и посулы интерпретируют своеобразно - если хотите хорошего, как бы не получилось еще хуже чем есть. Увы, это практически единственный из доступных нормально собирающийся lazarus под linux (не считая устаревших версий из официальных репозиториев того же debian и официальных релизов) и всегда корректно собирающийся под Windows, супротив например daily snapshots с сайта разработчиков, которые порою вообще неработоспособны.

Наша дата, наша точка отсчета: 18 июля 2012 года

Последний официальный релиз FreePascal 2.6.0, в нём UnicodeString являются алиасом WideStrings, поддержка этого строкового типа некорректна. Точнее, реализована более менее приемлемо только для операционных систем, основная локаль которых совпадает с локалью текстовой консоли. Либо, нужно программировать и устанавливать собственный wideStringManager.

Lazarus, а точнее, строковые функции из его модулей, на сегодняшний день UnicodeStrings из fpc не поддерживают никак, более того, часть его функций преобразования кодировок именно из-за неподдержки AnsiStrings с полями указания кодировки, может работать неправильно.

Автор не является ни участником команды разработки fpc, ни участником команды разработки lazarus, посему выраженное в тексте мнение является его личной точкой зрения, а все возможные прогнозы - предположениями, которые могут в действительности не состояться.

Насколько я понимаю, введение новых строковых типов и модификация старых, происходит в погоне за актуальностью соответствия новым версиям Delphi. В Delphi, напомним, катавасия с приравниваением Type String=UnicodeString и введением поля идентификации кодировки строк AnsiString, началась с версии 2009 (это насколько я помню, 2007-2008 год, да?), так что FreePascal под актуальность подгоняется действительно быстро, оперативно, и, что характерно, теми же способами - не так ли.

Про изменения в строках AnsiString

Теперь каждая такая строка имеет поле, указывающее кодировку содержащихся в ней символов. Поле это целого типа; константы, содержащиеся в модуле system, немногословны, из них интересны для реального использования, пожалуй только CP_NONE и CP_UTF8. Где надо задать cp1251 и cp866, можно так напрямую и использовать номера кодовых страниц - 1251 и 866 - номерки совпадают. Не забываем, что используемая кодовая страница должна поддерживаться операционной системой - работа идет через системные библиотеки.

Внутренней структуры записи, представляющей новую реализацию AnsiString, касаться не будем. Ибо, прямая модификация таких строк на уровне приложения есть занятие мазохистическое и неблагодарное; сторонники "оптимизации" пусть лучше задумаются над количеством необходимых изменений собственного кода, когда внутреннее представление строк в очередной раз поменяют.

Можно определить собственный строковый тип, с указанием привязанной к нему кодировки. Делается это довольно странной синтаксической конструкцией:

    Type CyrillicString = type Ansistring(1251);

Среди уже определенных в unit system "пользовательских строковых типов":

    Type RawByteString = type AnsiString(CP_NONE);
    Type UTF8String = type AnsiString(CP_UTF8);

Первая используется, когда необходимо подразумевать запрет перекодирования при присвоениях, вторая - для строк UTF8. Однако, не стоит создавать иллюзий - любые AnsiString есть байтовые строки, поэтому посимвольная индексация и стандартные функции позиционирования для UTF8String не работают, как и было ранее.

Символы unicode

Начнем с класса TCharacter, объявленного в unit character. Класс определен как sealed и полностью состоит из статических методов. Нет смысла создавать его экземпляры, можно напрямую действовать вызовами функций.

В TCharacter есть несколько полезных для общего назначения методов, например

для определения, цифра или нет знак в строке или отдельный символ:

    TCharacter.IsDigit(UnicodeString,Index):boolean;
    TCharacter.IsDigit(UnicodeChar):boolean;

для определения, является ли знак буквой:

   TCharacter.IsLetter(UnicodeString,Index):boolean;
   TCharacter.IsLetter(UnicodeChar):boolean;

под "буквой" здесь и далее подразумевается именно что буква любого языка, не обязательно из ASCII

   TCharacter.IsLower()
   TCharacter.IsUpper()

- для определения, к какому регистру относится указанный символ

   TCharacter.IsNumber()

- является ли знак числом в понятии Unicode (честно говоря, я не понял, чем IsNumber отличается по поведению от IsDigit)

   TCharacter.IsPunctuation

- для определения, является ли знаком пунктуации. Это следующие значки:

!"#%&'()*,-./:;?@[\]_{}
   TCharacter.IsSymbol

- небуквенные символы, не относящиеся к пробельным, разделителям и знакам препинания Собственно, это значки:

$+<=>^`|~

TCharacter.IsSeparator

- true дает почему то исключительно для пробела (#$20)

TCharacter.IsWhiteSpace
- здесь true для символов ASCII #9,#10,#11,#12,#32

TCharacter.ToLower
TCharacter.ToUpper
- переводят в верхний и нижний регистр как UnicodeChar, так и UnicodeString.

Некоторый интерес может представлять собой класс TEncoding из unit SysUtils. Однако, в отличие от прототипа, fpc пока не умеет писать/читать файлы с заданной через эту структуру кодировкой (имеются в виду операторы типа TStringList.SaveToFile("name.txt",TEncoding.Unicode))

В отличие от прототипа, класс SysUtils.TStringBuilder отсутствует

В отличие от прототипа, нет реализации AssignFile c указанием кодировки, то есть вот такого:

AssignFile(F1, 'MyFile.txt', CP_UTF8);

---

Строки UnicodeString и их использование

Для начала использования вы должны правильно сформировать заголовок файла.

Заголовок любого файла, входящего в проект, должен начинаться с директив:

    {$mode objfpc}
    {$H+}
    {$codepage UTF8}

Должен!!! Особо подчеркиваю обязательность наличия третьей строки, задающей кодовую страницу исходника. Соответствующий ключ командной строки компилятора, похоже, этой директиве не эквивалентен. Эстетствующие могут вместо UTF8 использовать cp1251, но не стоит себя тешить вредными привычками. Любителей текстов в кодировке cp866 (DOS) ждет грандиозный облом - компилятор неправильно преобразует строки на кириллице в исходниках этой кодировки.

Далее, для linux вы обязаны включить:

    uses
    {$IFNDEF WINDOWS}
    cwstring,
    {$ENDIF}

cwstring должен быть первым unit в списке основного модуля программного проекта. Без него работа со строками unicode в linux fpc невозможна. В качестве неявных зависимостей этот unit использует динамические библиотеки iconv и работает через них.

Определяем наши переменные:

    Var s,s1,s3:UnicodeString;
        uc:UnicodeChar;

следующее будет корректно работать:

    s:='Строка русского языка';
    s1:=UpCase(s);
    writeln('s =',s);
    writeln('s1=',s1);
    writeln('sl=',LowerCase(s1));
    for i:=1 to length(s) do begin
      uc:=s[i];
      write(uc,'|');
    end;
    write('Строка для ввода:');
    readln(s3);
    write('Введено:',s3);

Именно так, без применения всяких там UTF8ToConsole! Строки с русскими буквами корректно выводятся на экран, корректно вводятся с клавиатуры, символы их прекрасно индексируются, преобразуются к верхнему и нижнему регистрам соответсвующими функциями. Более того, содержимое переменных UnicodeStrings правильно отображается отладчиком lazarus!

Это тоже работает:

    i:=pos('русского',s);
    writeln('Position:',i);
    writeln(UpCase(Copy(s,i,8)));
    Delete(s,i,9);
    writeln(s);

причем, i именно позиция символа в строке, не байта!

Пишем файл.

    Var t:TextFile;
    ...
    assign(t, 'ИмяФайлаНаРусскомЯзыке.txt');
    rewrite(t);
    writeln(t,'Наш тестовый файлик');
    writeln(t,s);
    writeln(t,s1);
    close(t);

Да, действительно имя файла задано русским буквами. Файл создается и записывается корректно. С одним "но". Текст в нём будет представлен в кодовой странице, являющейся основной кодовой страницей операционной системы. Для windows это - 1251, для современных линуксов - вероятнее всего UTF8 (впрочем, вам виднее, как вы свой линукс наконфигурировали).

Если у нас появляются динамические строковые переменные "не unicode" заранее не определенной кодировки, преобразования при присвоении выполняются, неожиданно, по тем же правилам. Соответственно в присваивании UnicodeString в AnsiString в последней окажется в linux текст в UTF-8, в windows - текст в cp1251, что вообще то портабельной методикой назвать нелья. Это я хочу сказать, что если вы загоняете переменные типа UnicodeString в структуры типа TStringList напрямую, без промежуточной переменной AnsiString, то в списке окажется текст, помеченный кодировкой по представленному правилу, и к тому же в эту кодировку преобразованный средой исполнения.

Если необходимо вывести файл, содержащий строки не в системной кодировке, придется воспользоваться следующим приёмом (мы под Windows):

    Var rb:RawByteString;

    rb:='строка UTF8, преобразование которой не подразумевается';
    SetCodePage(rb,CP_NONE,FALSE);
    writeln(t,rb);

Для того, чтобы по этой же методике заставить в файл записаться строку в кодировке DOS (866), придется сделать (исходник у нас в UTF8):

    rb:='строка CP866, преобразование которой не подразумевается';
    SetCodePage(rb,866,TRUE);       // Конвертируем из UTF8 в cp866
    SetCodePage(rb,CP_NONE,FALSE);  // Снимаем принадлежность к кодовой странице без конвертирования
    writeln(t,rb);

Не забываем, что первым аргументом SetCodePage проходит только переменная типа RawByteString.

Исходя из практики применения, получается что функции из lconvencoding и lazutf8 на текущий момент не устанавливают поле кодовой страницы строки, строка после них получается в кодировке CP_NONE и при переприсвоении ее другим строкам, для которых кодировка задана, возможны и очень вероятны странности с кодировками. В свете тенденций развития я бы не стал полагаться на неизменность поведения функций работы со строками, входящих в библиотеки lazarus. Но и не забываем, что релизная версия Lazarus 1.0 готовится на сочетание с релизным же компилятором 2.6, который "правильный" unicodeString и описанные выше особенности поведения не поддерживает.

По поводу автоматического преобразования кодировок в AnsiString и UnicodeString

  • Переменные UnicodeString всегда помечены кодировкой 1200, при любом типе инициализации и присвоения.
  • AnsiString, объявленные как AnsiString, при инициализации строковой константой содержат кодировку исходника, объявленную директивой {$codepage}, дальнейшее поведение при присвоениях плохо прогнозируемо. Если первичное инициирование переменной происходит в результате присваивания какого либо строкового выражения, результат обычно будет в default кодировке операционной системы. Например (windows, исходник в UTF8):
       ac1:='Тестовая строка';   // ac1 65001
       ac2:=ac1;                 // ac2 65001
    
       но однако:
    
       ac1:='Тестовая строка';   // ac1 65001
       ac2:=ac1+ac1;             // ac2 1251
    
       ac1:=ac1+ac1;             // ac1 65001
    
  • Переменные "пользовательских" типов всегда сохраняют заданную кодовую страницу
  • Переменные RawByteString при инициализации их значением AnsiString с заданной кодовой страницей, принимают эту кодовую страницу, при инициализации строковым выражением оказываются в кодировке по умолчанию операционной системы. В дальнейших присвоениях любого содержимого не меняют однажды присвоенной кодировки.
        r1:=ac1;         // ac1 65001 -> r1 65001
        r1:=ac1+ac3;     // ac1 65001 ac3 1251 -> r1 1251 при начальной инициализации
        r1:=ac1+ac3;     // r1 65001 ac1 65001 ac3 1251 -> r1 65001 при повторном присвоении
    
    Кодировка для этих строк может быть изменена применением функции SetCodePage(строка,кодировка,перекодировать). Если последний параметр TRUE, уже имеющее содержимое строки будет конвертироваться из той кодировки, что была установлена ранее в ту, которая задается. Если предыдущее значение - CP_NONE, применяются правила преобразования по умолчанию, то есть предполагается что содержимое в кодировке по умолчанию операционной системы.

Все перечисленное выше дано как повод задуматься над тем, что вы более не вправе полагаться на внутреннее представление строк AnsiString, особенно при работе программы на разных платформах.

Уточняю: если ваша переменная или результат функции объявлен как String или AnsiString, нет абсолютных гарантий, что в ней окажется строка в UTF8, при наличии корректных исходных данных в UTF8. Решение есть - применять только строковые типы UnicodeString, либо переменные пользовательского типа с зафиксированной кодовой страницей, такие как UTF8String.

Если до сих пор непонятно, почему Lazarus сегодня разработчики категорически не рекомендуют комбинировать с компиляторами ветки 2.7, посмотрите, какие в его текстах используются строки для хранения информации UTF8.

Посмотреть, какая кодировка задана на текущий момент в строке, можно функцией StringCodePage.

Актуальные версии
FPC3.2.2release
Lazarus3.2release
MSE5.10.0release
fpGUI1.4.1release
links