处理符号和代码页

由于字符串是由字符组成的,有时候需要在整数代码层级操纵字符串中的单个字符或字符组,或者说这样更方便。例如,需要一次一个地读取或替换一个字符,或者将它们转换为整数代码数组,以便通过通信协议进行传输,或者转换为 动态库 DLL 的第三方编程接口。在所有这些情况下,将字符串作为文本传递可能面临各种困难:

  • 确保正确编码(有很多种编码,具体的选择取决于操作系统区域设置、程序设置、与之进行通信的服务器的配置等因素)
  • 本地文本编码与 Unicode 的国家/地区语言字符之间的相互转换
  • 以统一方式进行内存分配和解除分配

使用包含整数代码的数组(此类使用实际上生成了字符串的二进制表示而不是文本表示)会让这些问题变得简单。

MQL5 API 提供了对单个字符或字符组进行运算的一系列函数,这些函数已考虑到了编码特性。

MQL5 中的字符串包含两字节 Unicode 编码的字符。以单个(但非常大)字符表为所有各个国家的字母表提供了通用支持。两个字节即允许 65535 个元素的编码。

默认字符类型为 ushort。但在必要时,可将字符串以特定语言编码转换为单字节 uchar 字符系列。这种转换可能伴随某些信息的丢失(尤其是不在本地化字符表中的字母可能“丢失”变音符或甚至“转变”为某种替代字符:取决于上下文,其可能以不同方式显示,但通常显示为 ' ?' 或方块字符)。

为避免可能包含任意字符文本的问题,建议始终使用 Unicode。如果应与 MQL 程序集成的某些外部服务或程序不支持 Unicode,或者如果该文本从一开始就打算用于存储有限的字符集(例如,仅数字和拉丁字母),则可以例外。

与单字节字符相互转换时,MQL5 API 默认使用 ANSI 编码,具体取决于当前 Windows 设置。然而,开发人员可指定不同的码表(参见函数 CharArrayToStringStringToCharArray)。

这些函数的使用示例在 StringSymbols.mq5 文件中提供。

bool StringSetCharacter(string &variable, int position, ushort character)

该函数将传递的 variable 字符串中 position 处的字符更改为 character 值。数字必须在 0 到字符串长度 (StringLen) 减 1 之间。

如果要写入的字符为 0,则其指定一个新行结束(作为终止符零),即行长度变为等于 position。为该行分配的缓冲区大小不会改变。

如果 position 参数等于字符串长度,并且要写入的字符不等于 0,则该字符被添加到字符串,其长度增加 1。这等效于表达式:variable += ShortToString(character)

如果成功完成,该函数返回 true,如果出错,则返回 false

void OnStart()
{
   string numbers = "0123456789";
   PRT(numbers);
   PRT(StringSetCharacter(numbers70));   // cut off at the 7th character
   PRT(numbers);                             // 0123456
   PRT(StringSetCharacter(numbersStringLen(numbers), '*')); // add '*'
   PRT(numbers);                             // 0123456*
   ...
}

 

ushort StringGetCharacter(string value, int position)

该函数返回位于字符串中指定位置的字符的代码。位置编号必须在 0 到字符串长度 (StringLen) 减 1 之间。如果出错,该函数将返回 0。

该函数等效于使用运算符 '[]’ 写入:value[position]

   string numbers = "0123456789";
   PRT(StringGetCharacter(numbers5));      // 53 = code '5'
   PRT(numbers[5]);                          // 53 - is the same 

 

string CharToString(uchar code)

该函数将字符的 ANSI 码转换为单字符字符串。根据设置的 Windows 代码页,代码的上半部分(大于 127)可能生成不同的字母(字符样式不同,而代码保持相同)。例如,代码为 0xB8(十进制数 184)的符号在西欧语言中表示变音符,而在俄语中,此位置是字母 'ё'。再举一个例子:

   PRT(CharToString(0xA9));   // "©"
  PRT(CharToString(0xE6));   // "æ", "ж", or another character
                             // depending on your Windows locale

 

string ShortToString(ushort code)

该函数将字符的 Unicode 码转换为单字符字符串。对于 code 参数,可以使用字面量或整数。例如,希腊大写字母 "sigma"(数学公式中的求和符号)可指定为 0x3A3 或 'Σ'。

   PRT(ShortToString(0x3A3)); // "Σ"
   PRT(ShortToString('Σ'));   // "Σ"

 

int StringToShortArray(const string text, ushort &array[], int start = 0, int count = -1)

该函数将字符串转换为 ushort 字符码系列并拷贝到数组中的指定位置:从编号为 start(默认为 0,即数组开头)的元素开始,并且数量为 count

请注意,start 参数指数组中的位置,而不是字符串中的位置。如果想转换字符串的某一部分,必须首先使用 StringSubstr 函数来提取这一部分。

如果 count 参数等于 -1(或者 WHOLE_ARRAY),则将拷贝截至字符串末尾的所有字符(包括终止符 NULL)或者根据数组大小确定的字符数量(如果为固定大小)。

对于动态数组,如果必要,其大小将自动增加。如果动态数组的大小大于字符串的长度,则该数组的大小不减少。

要拷贝没有终止 null 的字符,必须将 StringLen 作为 count 自变量进行显式调用。否则,数组的长度将比字符串的长度大 1(而在最后一个元素为 0)。

该函数返回拷贝的字符数。

   ...
   ushort array1[], array2[]; // dynamic arrays 
   ushort text[5];            // fixed size array 
   string alphabet = "ABCDEАБВГД";
   // copy with the terminal '0'
   PRT(StringToShortArray(alphabetarray1)); // 11
   ArrayPrint(array1); // 65   66   67   68   69 1040 1041 1042 1043 1044    0
   // copy without the terminal '0'
   PRT(StringToShortArray(alphabetarray20StringLen(alphabet))); // 10
   ArrayPrint(array2); // 65   66   67   68   69 1040 1041 1042 1043 1044
   // copy to a fixed array 
   PRT(StringToShortArray(alphabettext)); // 5
   ArrayPrint(text); // 65 66 67 68 69
   // copy beyond the previous limits of the array 
   // (elements [11-19] will be random)
   PRT(StringToShortArray(alphabetarray220)); // 11
   ArrayPrint(array2);
   /*
   [ 0]    65    66    67    68    69  1040  1041  1042
         1043  1044     0     0     0     0     0 14245
   [16] 15102 37754 48617 54228    65    66    67    68
           69  1040  1041  1042  1043  1044     0
   */

请注意,如果拷贝位置大于数组大小,则中间元素将被分配,但不会初始化。因此,它们可能包含随机数据(上面以黄色高亮显示的部分)。

string ShortArrayToString(const ushort &array[], int start = 0, int count = -1)

该函数将数组带字符码的部分转换为字符串。数组元素的范围分别由 start(表示起始位置)和 count(表示数量)参数设置。start 参数值必须在 0 到数组元素数量减 1 之间。如果 count 等于 -1(或 WHOLE_ARRAY),则将拷贝截至数组末尾或截至第一个 null 的所有元素。

使用来自 StringSymbols.mq5 的相同示例,我们尝试将一个数组转换为大小为 30 的 array2 字符串。

   ...
   string s = ShortArrayToString(array2030);
   PRT(s); // "ABCDEАБВГД", additional random characters may appear here

由于在 array2 数组中,字符串 "ABCDEABCD" 被拷贝两次,具体为,第一次拷贝到开头位置,第二次拷贝到偏移 20 的位置,因此中间字符将为随机,能够形成一个比之前更长的字符串。

int StringToCharArray(const string text, uchar &array[], int start = 0, int count = -1, uint codepage = CP_ACP)

该函数将 text 字符串转换为一个单字节字符系列并拷贝到数组中的指定位置:从编号为 start(默认为 0,即数组开头)的元素开始,并且数量为 count。拷贝过程将字符从 Unicode 转换为选定代码页 codepage – 默认为 CP_ACP,即 Windows 操作系统语言(下文会详细介绍)。

如果 count 参数等于 -1(或 WHOLE_ARRAY),则将拷贝截至字符串末尾的所有字符(包括终止符 NULL)或者根据数组大小确定的字符数量(如果为固定大小)。

对于动态数组,如果必要,其大小将自动增加。如果动态数组的大小大于字符串的长度,则该数组的大小不减少。

要复制没有终止 null 的字符,必须将 StringLen 作为 count 自变量进行显式调用。

该函数返回拷贝的字符数。

参见文档中 codepage 参数的有效代码页列表。下面是一些广泛使用的 ANSI 代码页:

语言

代码

中欧拉丁语

1250

西里尔语

1251

西欧拉丁语

1252

希腊语

1253

土耳其语

1254

希伯来语

1255

阿拉伯语

1256

波罗的海语

1257

因此,在采用西欧语言的计算机上,CP_ACP 为 1252,而在采用俄语的计算机上,则为 1251。

在转换过程中,某些字符在转换后可能损失信息,因为 Unicode 表比 ANSI 表大得多(每个 ANSI 码表有 256 个字符)。

在这方面,CP_UTF8 在所有 CP_*** 常量中特别重要。它可以通过可变长度编码确保国家字符正确保留:生成的数组仍然存储字节,但每个国家字符可以跨多个字节,以特殊格式写入。因此,数组长度可能比字符串长度大得多。UTF-8 编码广泛用于互联网及各种软件。顺便说一下,UTF 代表 Unicode Transformation Format(统一码转换格式),还有其它修改格式,尤其是 UTF-16 和 UTF-32。

在我们熟悉“逆”函数 CharArrayToString 之后,我们将探讨 StringToCharArray 的示例:它们的工作原理必须共同演示。

string CharArrayToString(const uchar &array[], int start = 0, int count = -1, uint codepage = CP_ACP)

该函数将字节数组或其一部分转换为字符串。数组必须包含特定编码的字符。数组元素的范围分别由 start(表示起始位置)和 count(表示数量)参数设置。start 参数值必须在 0 和数组元素数量之间。当 count 等于 -1(或 WHOLE_ARRAY),则截至数组末尾或截至第一个 null 的所有元素被复制。

我们来了解函数 StringToCharArrayCharArrayToString 如何处理具有不同代码页面设置的不同国家字符。为此准备了测试脚本 StringCodepages.mq5

将使用两行作为测试对象,分别是俄语和德语:

void OnStart()
{
   Print("Locales");
   uchar bytes1[], bytes2[];
 
   string german = "straßenführung";
   string russian = "Russian text";
   ...

我们将其复制到数组 bytes1bytes2,然后将其还原为字符串。

首先,我们使用欧洲代码页 1252 转换德语文本。

   ...
   StringToCharArray(germanbytes10WHOLE_ARRAY1252);
   ArrayPrint(bytes1);
   // 115 116 114  97 223 101 110 102 252 104 114 117 110 103   0

在欧洲版本的 Windows 上,这等效于使用默认参数的较简单函数调用,因为其中 CP_ACP = 1252:

   StringToCharArray(germanbytes1);

然后我们使用以下调用将文本从数组还原,确保一切与原始内容匹配:

   ...
   PRT(CharArrayToString(bytes10WHOLE_ARRAY1252));
   // CharArrayToString(bytes1,0,WHOLE_ARRAY,1252)='straßenführung'

现在我们尝试以相同的欧洲编码转换俄语文本(或者可以在 CP_ACP 设置为 1252 作为默认代码页的 Windows 环境中调用 StringToCharArray(english, bytes2)):

   ...
   StringToCharArray(russianbytes20WHOLE_ARRAY1252);
   ArrayPrint(bytes2);
   // 63 63 63 63 63 63 63 32 63 63 63 63 63  0

你会看到转换期间出现问题,因为 1252 没有西里尔语。从数组还原字符串可清晰看到本质:

   ...
   PRT(CharArrayToString(bytes20WHOLE_ARRAY1252));
   // CharArrayToString(bytes2,0,WHOLE_ARRAY,1252)='??????? ?????'

我们在条件性俄语环境中重复试验,即我们使用西里尔语代码页 1251 双向转换字符串。

   ...
   StringToCharArray(russianbytes20WHOLE_ARRAY1251);
   // on Russian Windows, this call is equivalent to a simpler one
   // StringToCharArray(russian, bytes2);
   // because CP_ACP = 1251
   ArrayPrint(bytes2); // this time the character codes are meaningful
   // 208 243 241 241 234 232 233  32 210 229 234 241 242   0
   
   // restore the string and make sure it matches the original
   PRT(CharArrayToString(bytes20WHOLE_ARRAY1251));
   // CharArrayToString(bytes2,0,WHOLE_ARRAY,1251)='Русский Текст'
   
   // and for the German text...
   StringToCharArray(germanbytes10WHOLE_ARRAY1251);
   ArrayPrint(bytes1);
   // 115 116 114  97  63 101 110 102 117 104 114 117 110 103   0
   // if we compare this content of bytes1 with the previous version,
   // it's easy to see that a couple of characters are affected; here's what happened:
   // 115 116 114  97 223 101 110 102 252 104 114 117 110 103   0
   
   // restore the string to see the differences visually:
   PRT(CharArrayToString(bytes10WHOLE_ARRAY1251));
   // CharArrayToString(bytes1,0,WHOLE_ARRAY,1251)='stra?enfuhrung'
   // specific German characters were corrupted

这样,单字节编码的脆弱性就显而易见了。

最后,我们为两个测试字符串均启用 CP_UTF8 编码。不论 Windows 设置如何,示例的这一部分将能够稳定工作。

   ...
   StringToCharArray(germanbytes10WHOLE_ARRAYCP_UTF8);
   ArrayPrint(bytes1);
   // 115 116 114  97 195 159 101 110 102 195 188 104 114 117 110 103   0
   PRT(CharArrayToString(bytes10WHOLE_ARRAYCP_UTF8));
   // CharArrayToString(bytes1,0,WHOLE_ARRAY,CP_UTF8)='straßenführung'
   
   StringToCharArray(russianbytes20WHOLE_ARRAYCP_UTF8);
   ArrayPrint(bytes2);
   // 208 160 209 131 209 129 209 129 208 186 208 184 208 185
   //  32 208 162 208 181 208 186 209 129 209 130   0
   PRT(CharArrayToString(bytes20WHOLE_ARRAYCP_UTF8));
   // CharArrayToString(bytes2,0,WHOLE_ARRAY,CP_UTF8)='Русский Текст'

请注意,两个 UTF-8 编码的字符串需要的数组大小均超过 ANSI 编码字符串需要的数组。此外,俄语文本的数组实际上长度已变为 2 倍,因为所有字母现在占用 2 个字节。有兴趣者可在开源代码中找到有关 UTF-8 编码具体如何工作的详细信息。在本书中,重要的是 MQL5 API 提供了现成可用的函数。