字节、字符串
计算机中以八个二进制位表示一个八位字节 - 这称为一个单字节字符。
一组连续的字节就构成一个字符串,在aardio中字符串是基于二进制的,可以包含任何数据(例如图像、文本、或者'\0'等不可打印字符)。
//下面的代码定义了一个字符串变量str
var str = 'abcd中文\0\0\t'
上面的是一个最基础的字符串,注意上面的字符串放在单引号号,单引号中可以使用转义符,例如使用'\t'
表示制表符,放在双引号中则不能使用转义符(所以双引号中只能包含可见的文本字符),关于怎么表示一个字符串的更多语法请参考aardio语法手册,这里不再详述。
字节码
每个八位字节实际上存储在内存里就是一个数值 - 我们把这个值称之为字节码。
在 aardio 中你可以把字符串理解为一个字节数组,例如 str[1]
读取字符串的第一个字节码(数值类型),而 str[[1]]
则返回一个字符(字符串类型)
在aardio 是可以使用 #str 获取一个字符串包含的字节数(也就是字符串长度)。
请运行下面的 aardio 代码:
import console;
console.log("ab的长度为2" ,#"ab" ); //每个英文字符占一个字节
console.log("'中文'的长度为6" ,#"中文" ); //UTF8是多字节变长编码,这里的两个汉字占用6字节
console.pause(true);
可以看到一个英文字符占1个字节,而一个汉字占用多个字节。
文本字符串、二进制字符串
'\0'
表示字符串终止,而 UTF-16 编码的字符串以'\u0000'
为终止符。很明显的,文本字符串不能在内容中包含终止符。二进制字符串
二进制字符串指的是可以包含任何字节码,当然也可以包含文本,文本也是二进制,但我们一般说的二进制字符串指的是他不仅仅可以包含文本,目的也不仅仅是用于直接打印显示。显然,二进制字符串可以在内容中包含'\0'
,'\u0000'
这些,不再是终止符。二进制字符串会自己记录自己的长度,而不是依赖终止符去获取长度。
aardio中的字符串是二进制字符串,记录自己的长度而不是依赖终止符取长度,例如在C语言中用 strlen(str) 取一个字符串的长度,就需要一个字节一个字节的往后找 - 直到遇到终止符'\0'
,Unicdoe字符串也类似,所以把 aardio 的字符串传入C语言等实现的接口参数时,aardio还是会保证字符串尾部有终止符'\u0000'
以避免越界操作,这个保护性的终止符是隐藏的( 在aardio字符串中看不到这个终止符,取字符串长度也不会包含终止符 )。在 aardio 中用 raw.buffer()
分配内存缓冲区时,也会有类似的机制在尾部隐藏的放置一个保护性的终止符 '\u0000'
(你在aardio代码中访问不到他,取长度也不会包含终止符)。
字符编码、字符集、ASCII编码
请在 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个二进制位来存储。实际上理论上来说你可以自己设计一套编码规则,例如你可以规定:
ANSI已经制定了ASCII编码用于表示单字节字符集,常用的英文字符即是使用ASCII编码表示,例如上面的97( 2#1100001 ) 表示字母"a"就是ASCII规定的。大家可以自行网上搜索ASCII码表看一看了解一下,这里不再详述。
多字节字符集
用单个字节(8个二进制位)能表示的字符数量非常有限,这对英文世界不是问题,但对中、日、韩这样的文字就是问题了,所以各个有了各个国家不同的多字节编码方案,用多个字节来表示一个字符,例如中文里的GBK(GB2312)编码,繁体中文的BIG5编码,日文的Shift-JIS编码,多字节编码即 MBCS(Multi-Byte Chactacter System),MBCS的常见实现是DBCS(Double-Byte Character Set) - 也就是双字节字符集,双字节字符集的基本编码规则如下:
'\x80'
是欧元符号(也就是最大的一个单字节码),ASCII中小于32的是控制字符,至于这个32就是空格符了。多字节编码的好处是比较节省存储空间,兼容一些二进制的字符串操作,但是有一个比较麻烦的是,例如你用一个单字节字符来分隔字符串,但是他找啊找找到某个双字节字符的尾字节,然后拆分什么的,出来的结果就乱套、乱码了(这个问题我们称之为“串码”),所以这种多字节处理起来就比较麻烦(你不能直接用处理二进制字符串的代码去处理文本,因为你得不停的分析某个字节是不是一个多字节字符的尾字节),另外各国使用的编码不一致,这就导致经常出现乱码等问题。
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 编码:
//把 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 它自己。
UTF-8 编码
UTF-8编码是变长编码,但他有一个好处是很像多字节编码,兼容单字节编码,类似英文ASCII字符这些仍然只要一个字节存储,而且编码与ASCII兼容。UTF-8的代码是变长的,其编码规则如下:
/*
* 0000 0000-0000 007F - 0xxxxxxx 单字节
* 0000 0080-0000 07FF - 110xxxxx 10xxxxxx 双字节
* 0000 0800-0000 FFFF - 1110xxxx 10xxxxxx 10xxxxxx 3字节
*/
Unicode 0080 ~07FF 转换为UTF8需要2字节,例如 \u00CA (11001010)
转换为UTF8过程如下:
Unicode 0800 ~FFFF 转换为UTF8需要2字节,例如\uF03F (11110000 0011111)
转换为UTF8过程如下:
UTF8的编码比较特别,小于0x80(最大0x7F)一定是单字节,多字节的前导字节用前导二进制位表示自己有几个字节,附加字节总是10xxxxxx,所以比较容易用代码分析一段字符串是否使用UTF8编码,例如Windows上的记事本就是通过这种机制自动识别一个文本文件的编码,aardio中也有一个类似的函数,即 string.isUtf8(字符串) , 这个函数如果检测到一个合法的UTF8编码的字符串会返回true。
UTF8中一个字符有多个字节时 - 每个字节一定不会小于 0x80,这带来一个好处:以处理二进制字符串的函数(例如模式匹配)操作ASCII字符不会出现MBCS编码那样的串码问题。但如果用支持MBCS编码的代码去处理UTF-8字符串会出问题,因为在UTF8中一个三字节的中文字符、加上一个单字节的英文字符,用支持MBCS编码的函数去处理,会被理解还两个双字节字符,结果就乱了, 下面我们用一段 aardo 代码演示一下这种乱码出现的过程:
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 字符串自带 UTF标记,具有 Unicode 编码自我识别能力,
而 '\xEF\xBF\xBD\xEF\xBF\xBD\xEF\xBF\xBD' 是合法的 UTF-8 编码,
所以下面的字符串会显示正确的字符:���
*/
console.log('\xEF\xBF\xBD\xEF\xBF\xBD\xEF\xBF\xBD' ) //输出:���
/*
下面这样也改变不了编码,
因为 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);