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;
Неожиданно с кодировкой в момент компиляции
ОтветитьУдалить