# 理解字符串与 UTF-8 编码

1. 字节、字符串  

    计算机中以八个二进制位表示一个八位字节 - 这称为一个单字节字符。 

    一组连续的字节就构成一个字符串，在aardio中字符串是基于二进制的，可以包含任何数据（例如图像、文本、或者'\\0'等不可打印字符）。

    ```aardio
    //下面的代码定义了一个字符串变量str  
    var str = 'abcd中文\0\0\t'
    ``` 

    上面的是一个最基础的字符串，注意上面的字符串放在单引号号，单引号中可以使用转义符，例如使用`'\t'`表示制表符，放在双引号中则不能使用转义符（所以双引号中只能包含可见的文本字符），关于怎么表示一个字符串的更多语法请参考aardio语法手册，这里不再详述。 

2. 字节码
 
    每个八位字节实际上存储在内存里就是一个数值 - 我们把这个值称之为字节码。

    在 aardio 中你可以把默认的字符串理解为一个只读的字节串，例如 `str[1]` 读取字符串的第一个字节码（数值类型），而 `str[[1]]` 则返回一个字符（字符串类型）。

    在 aardio 是可以使用 #str 获取一个字符串包含的字节数（也就是字符串长度）。

    请运行下面的 aardio 代码：

    ```aardio
    import console;  
    console.log("ab的长度为2" ,#"ab" ); //每个英文字符占一个字节  
    console.log("'中文'的长度为6" ,#"中文" ); //UTF8是多字节变长编码，这里的两个汉字占用6字节  
    console.pause(true);
    ```

    可以看到一个英文字符占1个字节，而一个汉字占用多个字节。

    > UTF-16 编码的字符串（例如 `UTF-16`u ）使用 `[]` 或 `[[]]` 操作符时以 2 个字节为一个单位，但用 `#` 操作符取长度时仍以单个字节计数。

  
3. 文本字符串、二进制字符串  
  
    1. 文本字符串  
    文本字符串只包含可正常打印、直接显示的字符，一般的单字节、多字节编码的字符串以`'\0'`表示字符串终止，而 UTF-16 编码的字符串以`'\u0000'`为终止符。很明显的，文本字符串不能在内容中包含终止符。  
    
    2. 二进制字符串  
    二进制字符串指的是可以包含任何字节码，当然也可以包含文本，文本也是二进制，但我们一般说的二进制字符串指的是他不仅仅可以包含文本，目的也不仅仅是用于直接打印显示。显然，二进制字符串可以在内容中包含`'\0'`,`'\u0000'`这些，不再是终止符。二进制字符串会自己记录自己的长度，而不是依赖终止符去获取长度。  
    
        aardio中的字符串是二进制字符串，记录自己的长度而不是依赖终止符取长度，例如在C语言中用 strlen(str) 取一个字符串的长度，就需要一个字节一个字节的往后找 - 直到遇到终止符`'\0'`，Unicdoe字符串也类似，所以把 aardio 的字符串传入C语言等实现的接口参数时，aardio还是会保证字符串尾部有终止符` '\u0000'` 以避免越界操作，这个保护性的终止符是隐藏的（ 在aardio字符串中看不到这个终止符，取字符串长度也不会包含终止符 ）。在 aardio 中用 `raw.buffer()` 分配内存缓冲区时，也会有类似的机制在尾部隐藏的放置一个保护性的终止符 `'\u0000'`（你在aardio代码中访问不到他，取长度也不会包含终止符）。 

4. 字符编码、字符集、ASCII 编码


    请在 aardio 中运行下面的代码，查看控制台输出：

    ```aardio
    import console;
    var str = "ab";  
    console.log("输出'a'的字节码97",str[1]);  
    console.log("输出字符'b'",str[[2]]);  
    
    console.pause(true);
    ``` 

    实际上"a" 在内存中存储的值就是一个数值 97 ，而97转换的2进制为 `1100001`，

    可以看到一个字节码使用了8个二进制位来存储。实际上理论上来说你可以自己设计一套编码规则，例如你可以规定：
    - 1000001 表示"a"  
    - 1000011 表示"b"

  
    ANSI已经制定了ASCII编码用于表示单字节字符集，常用的英文字符即是使用ASCII编码表示，例如上面的97( 2#1100001 ) 表示字母"a"就是ASCII规定的。大家可以自行网上搜索ASCII码表看一看了解一下，这里不再详述。  
    
  
5. 多字节字符集  
  
    用单个字节（8个二进制位）能表示的字符数量非常有限，这对英文世界不是问题，但对中、日、韩这样的文字就是问题了，所以各个有了各个国家不同的多字节编码方案，用多个字节来表示一个字符，例如中文里的GBK（GB2312）编码，繁体中文的BIG5编码，日文的Shift-JIS编码，多字节编码即 MBCS(Multi-Byte Chactacter System)，MBCS的常见实现是DBCS（Double-Byte Character Set） - 也就是双字节字符集，双字节字符集的基本编码规则如下：  
  
    1. 小于等于0x80的字节码表示ASCII单字节字符,英文字母数字符号等，注意`'\x80'`是欧元符号(也就是最大的一个单字节码)，ASCII中小于32的是控制字符，至于这个32就是空格符了。  
    2. 大于0x80的字节表示他是一个双字节字符，他后面还跟了一个尾字节共同组成一个字符，常见的几种中日韩编码（代码页 936，950，932，949）尾字节不小于0x40，这个0x40就是"@"这个字符，所以理论上字节码小于"@"的一般做二进制的搜索拆分就比较安全。  
  
    多字节编码的好处是比较节省存储空间，兼容一些二进制的字符串操作，但是有一个比较麻烦的是，例如你用一个单字节字符来分隔字符串，但是他找啊找找到某个双字节字符的尾字节，然后拆分什么的，出来的结果就乱套、乱码了（这个问题我们称之为“串码”），所以这种多字节处理起来就比较麻烦（你不能直接用处理二进制字符串的代码去处理文本，因为你得不停的分析某个字节是不是一个多字节字符的尾字节），另外各国使用的编码不一致，这就导致经常出现乱码等问题。  
  
6. ANSI 代码页( Code Page )，Unicode 编码。  
  
    各种语言使用的多字节字符集并不统一，ANSI 代码页就是用来告诉操作系统当前使用的是哪种语言的字符集，例如把系统代码页设为 936 就支持简体中文，而繁体 BIG5 编码的代码页就是 950。  
    
    因为不同的字符集并不统一，所以就有了 Unicode 统一编码这种东西，各种语言在这个 Unicode 里的编码都是统一的，这个 Unicode 的知识这里就不细讲了，网上太多。Unicode有多种不同的编码方案，常用的是 UTF-16 LE, UTF-8 , UTF-16 每个字符用两个字节表示，aardio 中一般没有特定说明，函数或文档中提到的 UTF-16 都是指 UTF-16 LE，另外还有一个UTF-16 BE，LE 是小端序，BE 是大端序，这两种编码的区别是字符串中两个字节的前后位置相反。  
  
    UTF-8 他的编码规则很像多字节字符集，是变长编码，而且他跟其他多字节字符集一样，可以方便地转换到其他代码页，例如在 aardio 中可以这样转换 UTF-8 编码：  

    ```aardio
    //把 UTF-8 编码转换为 GBK 编码  
    str = string.fromto('UTF-8 字符串',65001,936)
    ``` 

    65001 就是 UTF-8 的代码页，936 是简体中文的代码页。因为各种代码页中的字符基本都能在Unicode 中找到位置，所以把一种编码转换到 Unicode( UTF-16  ) ，再从 Unicode( UTF-16  )  转换到其他编码，就实现了编码的相互转换。
    
    aardio 里实现这个功能的就是 string.fromto 函数， string.fromto() 函数就先调用 string.toUtf16() 将字符串的编码转换到 Unicode( UTF-16  )，然后再调用 string.fromUtf16() 将字符串从 Unicode( UTF-16  ) 转换到指定编码。 

    UTF-16 LE 的代码页是 1200，UTF-16 BE 的代码页是1201。string.fromto() 函数也可以支持这两个代码页，也就是说 string.fromto 的参数可以指定任意编码方案，包括 UTF-16 它自己。

    aardio 中可以在单引号包围的字符串后面加 u 后缀创建一个字面值的 UTF-16 字符串，例如 `'UTF-16 字符串'u`，UTF-16 字符串使用 `[]` 或  `[[]]` 读写时都是以点用两个字节的宽字符为一个单位，但用 `#` 操作符取长度时仍然是按单字节计数。

7. UTF-8 编码

    aardio 字符串是二进制兼容的，当包含文本时默认使用 UTF-8 编码。

    UTF-8 编码是变长编码，但它有一个好处是很像多字节编码，兼容单字节编码，类似英文 ASCII 字符这些仍然只要一个字节存储，而且这种单字节编码与 ASCII 兼容。
    
    UTF-8 的代码是变长的，其编码规则如下：

    ```aardio
    /*  
    * 0000 0000-0000 007F - 0xxxxxxx 单字节  
    * 0000 0080-0000 07FF - 110xxxxx 10xxxxxx 双字节  
    * 0000 0800-0000 FFFF - 1110xxxx 10xxxxxx 10xxxxxx 3字节  
    */
    ```

    Unicode 0080 ~ 07FF 范围的字符用 UTF-8 编码时需要 2 个字节，例如 `\u00CA (11001010)`转换为 UTF-8 过程如下：  
    - 将二进制 `11001010` 拆分为 `00011 001010`，位数不足在前面补 0。
    - 将后半部分 `001010` 填充到 `110xxxxx 10xxxxxx` 的第二个字节变成 `110xxxxx 10001010`
    - 将前半部分 `00011` 填充到 `110xxxxx 10001010` 的第一个字节变成 `11000011 10001010`，不足在前面补 0。 

    ![2 字节 UTF-8 编码图示](./images/utf-8-2.gif)

    Unicode 0800 ~FFFF 范围的字符转换为 UTF-8 编码则需要 3 字节，例如`\uF03F (11110000 0011111)` 转换为 UTF-8 过程如下：  
    - 将二进制 `11110000 0011111` 拆分为 `1111 000000 111111`，位数不足在前面补 0。
    - 将最后部分 `111111` 填充到 `1110xxxx 10xxxxxx 10xxxxxx` 尾部字节变成 `1110xxxx 10xxxxxx 10111111`
    - 将中间部分 `000000` 填充到  `1110xxxx 10xxxxxx 10111111`
    的倒数第 2 个字节变成 `1110xxxx 10000000 10111111`
    ，不足在前面补 0。
    - 将最前部分的 `1111` 填充到 `1110xxxx 10000000 10111111` 的第一个字节变成  `11101111 10000000 10111111`。

     ![3 字节 UTF-8 编码图示](./images/utf-8-3.gif)

    UTF-8 编码中小于 0x80(最大0x7F) 的一定是单字节，多字节字符的第一个前导字节用前导二进制位说明自己有几个字节，其他附加字节的二进制位总是 10xxxxxx，所以比较容易用代码分析一段字符串是否 UTF-8 编码。aardio 中提供了 string.isUtf8(字符串) 函数用于检测字符串是否符合 UTF-8 编码规则。  
    
    UTF-8 中一个字符有多个字节时 - 每个字节一定不会小于 0x80，这带来一个好处：以处理二进制字符串的函数（例如模式匹配）操作ASCII字符不会出现 MBCS 编码那样的串码问题。但如果用支持MBCS编码的代码去处理UTF-8字符串会出问题，因为在UTF8中一个三字节的中文字符、加上一个单字节的英文字符，用支持MBCS编码的函数去处理，会被理解还两个双字节字符，结果就乱了, 下面我们用一段 aardo 代码演示一下这种乱码出现的过程:  
  

    ```aardio
    import console;/*  
    有一些用 Unicode 是没法表示的编码（无效码点），  
    Unicode 规定用 Replacement Character( \uFFFD ) 表示（显示为：� ）。  
    */  
    var str = '\uFFFD\uFFFD\uFFFD'  
    
    //\uFFFD 在 UTF8 中会编码为 \xEF\xBF\xBD  
    console.log('\xEF\xBF\xBD'=='\uFFFD') //输出：true  
    
    /*  
    '\xEF\xBF\xBD\xEF\xBF\xBD' 按 GBK编码 2个字节一个字符就是 "锟斤拷"  
    因为 aardio 中的控制台兼容 ANSI 编码，所以可以这样写  
    */  
    console.log('\xEF\xBF','\xBD\xEF','\xBF\xBD');//输出：锟  斤 拷  
    
    // aardio 代码中用字面值表示的字符串具有 Unicode 编码自我识别能力，并且自动设置合适的 UTF 标记。 
    console.log('\xEF\xBF\xBD\xEF\xBF\xBD\xEF\xBF\xBD' ) //输出：��� ，这是合法的 UTF-8 编码。
    
    /*  
    下面这样也改变不了编码，  
    因为 aardio 字符串带 UTF 标记，能自动纠正错误的编码转换。  
    */  
    var str  = string.fromto('\xEF\xBF\xBD\xEF\xBF\xBD\xEF\xBF\xBD',936,65001)  
    console.log(str=='\xEF\xBF\xBD\xEF\xBF\xBD\xEF\xBF\xBD') //输出：true  
    
    /*  
    如果创建 buffer 类型的二进制字节串，逃脱 aardio 的 UTF 标记检测，就可以重现这个 BUG 。  
    */  
    var str  = string.fromto(raw.buffer('\xEF\xBF\xBD\xEF\xBF\xBD\xEF\xBF\xBD'),936,65001)  
    console.log(str) //输出：锟斤拷锟?  
    
    console.log("ANSI 编码仅仅是历史包袱，已经没有存在的意义和使用价值。 ")  
    console.pause(true);
    ```
