24 декабря 2009 г.

Ответ на задачку №3

Ответ на задачку №3. Проблема тут в том, что в unicode Delphi (Delphi 2009 и выше) обычные старые-добрые AnsiString-строки поменяли свой формат и теперь они хранят информацию о кодировке своего текста.

Напомню, что раньше (Delphi 2007 и ниже) AnsiString = RawByteString и хранила данные без учёта их кодировки. Т.е. в одной и той же строке могли лежать Win1251, OEM 866 и UTF-8. В новых Delphi (Delphi 2009 и выше), помимо введения Unicode, старые Ansi-строки теперь также имеют информацию о кодовой странице, хранящихся в них данных. Казалось бы, полезное и правильное нововведение, которого долго ждали, но...

Итак, напомню код:
var
  Text, Data: AnsiString;
begin
  // откуда-то получаем данные. Например - от GetWindowTextA
  Data := ...;             // шаг №1

  // Теперь сам код
  Text := 'Text ' + Data;  // шаг №2

  ShowMessage(Text);       // шаг №3
end;
Окей, шаг №1 - в Data лежит строка 'Тест', кодировка - Win1251. Об этом сказано в условии задачи. Грубо говоря, это означает, что мы запустили программу на русской Windows с русскими настройками (язык для не-Unicode программ = Русский).

Шаг №3 интереса не представляет - об этом также было сказано в условии ("Трансформация из unicode в ANSI и наоборот не являются проблемой в этой задаче").

Вся проблема в шаге №2. Сам шаг делится на две части: конкатенация строк и сохранение результата в переменную.

Итак, на входе мы имеем: строковую константу и строковую переменную с данными в Win1251. На выходе у нас должна получиться строковая переменная.

Вопрос №1: какую кодировку будет иметь результирующая переменная Text?

Хм, до присваивания она не имела кодировки (не была инициализирована вообще). логично предположить, что она будет иметь кодировку результата конкатенации, верно?

Тогда, вопрос №2: какую кодировку будет иметь результат конкатенации? Ну, смотрите сами: мы соединяем две Ansi строки. Если бы мы соединяли две Unicode строки - проблем бы не было: мы просто присоединяем одну к другой, побайтно копируя содержимое, и всё.

Но для Ansi-строк мы так поступить не можем. Почему? Потому что Ansi строки могут содержать данные в разных кодировках! Наверное, будет не очень хорошо, если к UTF-8 строке мы добавим байты из строки с Win1251, получая в результате не читаемый бред (байты Win1251 будут трактоваться как UTF-8, что, очевидно, не приведёт ни к чему хорошему).

Примечание: в старых Delphi вам нужно было следить за этим самим, вручную. Поскольку Ansi-строки не несли информации о кодировках, то при соединении двух строк вы бы как раз и получили бы байты Win1251 в строке, трактуемой как UTF-8.

Вопрос №3: как тогда соединяются две Ansi-строки в новых Delphi? Хм, в документации освещения этого механизма я не нашёл. Но логически можно предположить (а потом на практике и проверить). Если мы соединяем две строки с разными кодировками, то результат будет иметь кодировку первой строки (при отсутствии других факторов). Простое и логичное правило. О "других факторах" я скажу чуть ниже.

Это находит подтверждение и в коде RTL Delphi:
  // _LStrCat
  asm
  // ...
    @@sourceIsAnsi:
           MOV     EDI,[ECX-skew].StrRec.length
           MOV     EDX,[ESI-skew].StrRec.length
           ADD     EDX,EDI
           JO      @@lengthOverflow
           MOV     EAX,EBX
           MOVZX   ECX,[ECX-skew].StrRec.codePage // использовать кодовую страницу первого источника
  // ...
  end;
Вопрос №4: а какую кодировку будет иметь строковая константа 'Test '? Unicode? Но ведь мы присоединяем константу к Ansi-строке, так что это будет Ansi-константа, а вовсе не Unicode. Текущую кодировку (Win1251)? Хм, но это же константа! Она не может меняться на этапе run-time. Значит, кодировка константы жёстко зашита в программу. Чему же она равна? Логично предположить, что она равна текущей кодировке в момент компиляции программы.

Ага, вот и оно. Если вы скомпилируете код выше на, скажем, английской Windows, то 'Test ' получит кодировку Win1252, следовательно, конкатенация 'Test ' + Data будет трактоваться как Win1252 + Win1251 и результат получит кодировку Win1252, в результате чего мы потеряем все символы из кириллицы. Результат выполнения кода будет 'Test Òåñò' (кодировка: Win1252) вместо ожидаемого 'Test Тест' (кодировка: Win1251).

Где и на что это влияет? Если вы пишете программу избегая Ansi-строк - вам на это наплевать. Даже если вы используете Ansi-строки, вам может быть это по боку, если вы используете Ansi-строки только в крайних случаях: непосредственно перед передачей/приёмом данных. Т.е. если вы не производите операций с ними, сразу же приводя строки в обычный String.

Далее, это не повлияет на вас, если вы пишете программу для одного языка (например, русского). В этом случае идите в настройки и на вкладке Delphi Compiler/Compiling установите CodePage в 1251. Этот параметр отвечает за кодировку всех Ansi-констант (он не влияет на кодировку Unicode-констант или операции с Ansi-строками). Если он равен 0 (по-умолчанию), то строковые Ansi-константы будут иметь текущую (в момент компиляции) кодировку. Если же вы укажете значение явно - будет использоваться именно оно. При этом, уже не имеет значения, в каком окружении вы будете компилировать программу.

Однако, если вы пишете программу, которая должна работать с текущей системной кодовой страницей, а не с фиксированной (т.е. и на русской и на немецкой Windows), и при этом ваша программа обрабатывает Ansi-строки - вы попали.

Подзадачка 3.5: как это обойти?

Для начала перечислю, что НЕ будет работать:
  • Любые преобразования типов. AnsiString('Test ') и RawByteString('Test ') ничего не дадут, т.к. они выполняются на этапе компиляции, и всё равно строковая константа будет иметь фиксированную кодовую страницу.
  • Замена константы переменной. Вводя временную переменную D (любого типа): D := 'Test';, вы могли бы переписать код так: Text := D + Data;. Это ни на что не повлияет, т.к. D будет иметь кодировку Win1252 и всё вышесказанное будет в силе, за исключением того, что вместо первого аргумента у нас теперь переменная, а не константа. Но все кодировки останутся на своих местах.
Что будет работать?

1. Ну, очевидно, что мы можем установить кодовую страницу вручную, например, так:
var
  Text, Data: AnsiString;
  D: RawByteString;
begin
  // откуда-то получаем данные. Например - от GetWindowTextA
  Data := ...;                                 // шаг №1

  // обработка константы
  D := 'Text ';
  SetCodePage(D, StringCodePage(Data), False); // шаг №2.1

  // Теперь сам код
  Text := D + Data;                            // шаг №2.2

  ShowMessage(Text);                           // шаг №3
end;
Кошмар, не правда-ли?

2. Мы можем воспользоваться "особыми факторами" при конкатенации. Это будет самым удобным способом, если это применимо. Например:
var
  Text, Data: AnsiString;
begin
  // откуда-то получаем данные. Например - от GetWindowTextA
  Data := ...;             // шаг №1

  // Хак
  Text := Data;            // шаг №2.1

  // Теперь сам код
  Text := 'Text ' + Text;  // шаг №2.2

  ShowMessage(Text);       // шаг №3
end;
Как это работает? Ну, в данном случае кодовая страница результата будет Win1251, т.к. она будет браться со второго аргумента конкатенации, а не с первого, как в исходном случае. Почему? Что изменилось?

В первом случае у нас оба аргумента были равноправны, поэтому компилятор выбирал первый из них. Во втором случае у нас другая ситуация: обратите внимание, что в конкатенации используется та же переменная, которой присваивается результат (Text). В первом же случае оба аргумента были свободными.

Что это значит? Что у нас теперь не просто конкатенация двух строк, а у нас, фактически, дописывание другой строки (в нашем случае: константы) к данной строке (Text) слева. Поэтому кодировка результата уже жёстко фиксирована: она равна кодировке строки, к которой производится дописывание (т.е. Text). А у нас там - Win1251. Поэтому во втором случае происходит конвертация 'Test ' (Win1252) в 'Test ' (Win1251) и прикрепление её к строке Text с сохранением кодировки Win1251.

3. Не менее отлично будет работать и использование Unicode-текстовых констант, вместо Ansi-констант. Только делать это надо не преобразованием типа (оно не будет работать, как уже указано выше), а вот так:
var
  Text, Data: AnsiString;
  D: String;
begin
  // откуда-то получаем данные. Например - от GetWindowTextA
  Data := ...;             // шаг №1

  // Хак:
  D := 'Test ';            // шаг №2.1

  // Теперь сам код
  Text := D + Data;        // шаг №2.2

  ShowMessage(Text);       // шаг №3
end;
Понятно, что в этом случае у нас будет конкатенация Unicode строки с Ansi-строкой, поэтому кодировка результата будет определяться Ansi-строкой, что есть Data с её Win1251.

4. Обратите внимание, что в данном примере важен порядок: константа, затем переменная. В обратном случае проблем нет:
var
  Text, Data: AnsiString;
begin
  // откуда-то получаем данные. Например - от GetWindowTextA
  Data := ...;             // шаг №1

  // Теперь сам код
  Text := Data + ' Text';  // шаг №2

  ShowMessage(Text);       // шаг №3 - выведет 'Тест Test'
end;

Как поэкспериментировать?

Ну, вам вовсе не обязательно иметь две машины для экспериментов с этим подводным камнем.

Эту ситуацию можно эмулировать так: установите параметр CodePage проекта = 1252 и используйте такой код:
var
  Text, Data: AnsiString;
  R: RawByteString;
begin
  SetLength(R, 4);
  SetCodePage(R, 1251, False);
  R[1] := AnsiChar(210);
  R[2] := AnsiChar(229);
  R[3] := AnsiChar(241);
  R[4] := AnsiChar(242);

  // откуда-то получаем данные. Например - от GetWindowTextA
  Data := R;               // шаг №1

  // Теперь сам код
  Text := 'Text ' + Data;  // шаг №2

  ShowMessage(Text);       // шаг №3
end;

1 комментарий :

  1. Неожиданно с кодировкой в момент компиляции

    ОтветитьУдалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и (опционально) ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.