本 EEP 建议在 Erlang 中使用 Unicode 字符的标准表示形式,以及处理它们的基本功能。
随着 Unicode 字符的广泛使用,需要在 Erlang 中对 Unicode 字符进行通用表示。到目前为止,编写 Unicode 程序的 Erlang 程序员必须自行决定其表示形式,并且几乎没有来自标准库的帮助。
在库中实现处理 Erlang 中 Unicode 表示的所有可能组合和变体的函数被认为是极其耗时的,并且会给标准库的未来用户带来困惑。
因此,需要一种处理二进制文件和列表的通用表示形式,从而使标准库中 Unicode 的处理更容易实现并产生更严格的结果。
一旦就表示形式达成一致,就可以逐步完成实现。本 EEP 仅概述了系统应提供的最基本的功能。如果实施此 EEP,Unicode 支持绝不是完整的,但实现将是可行的。
EEP 还建议使用库函数和位语法来处理替代编码。但是,建议使用一种标准编码,这将是 Erlang 中的库函数所期望支持的,而其他表示形式仅在转换方面得到支持。
Erlang 传统上将文本字符串表示为字节(8 位实体)列表,其中字符以 ISO-8859-1 (latin1) 编码。
随着 Unicode 字符的使用越来越广泛,需要一种关于如何在 Erlang 中表示 Unicode 字符的通用观点。
Unicode 是一种字符编码标准,其中所有已知的、现有的和历史的书面语言都用一个字符集表示,这当然会导致字符需要超过 8 位才能表示。
无论采用何种表示形式,Unicode 字符集都是 latin1 字符集的超集,而 latin1 字符集反过来又是传统的 7 位 US-ASCII 字符集的超集。因此,在 Erlang 列表中表示 Unicode 字符非常自然地是通过允许列表中的字符取大于 255 的值来完成的。
因此,在 Erlang 中,Unicode 字符串可以方便地存储为列表,其中每个元素代表一个 Unicode 字符。以下列表
_ex1:
[1050,1072,1082,1074,
1086,32,1077,32,85,110,105,99,111,100,101,32,63]
- 将表示“什么是 Unicode?”的保加利亚语翻译(看起来像“KAKBO e Unicode?”,只有最后一部分是拉丁字母)。最后一部分 ([32,85,110,105,99,111,100,101,32,63]
) 是普通的 latin1,因为字符串“Unicode?”是用拉丁字母书写的,而第一部分包含不能用单个字节表示的字符。本质上,该字符串以 Unicode 编码标准 UTF-32 编码,每个字符一个 32 位实体,这对于每个位置一个 Unicode 字符来说绰绰有余。
但是,当前最常见的 Unicode 字符表示形式是 UTF-8,其中字符存储在一个到四个 8 位实体中,其组织方式是,普通的 7 位 US ASCII 不受影响,而 128 及以上的字符则分布在多个字节中。这种编码的优点是,例如,对文件/操作系统有意义的字符保持不变,并且许多西方语言中的字符串在转换为 Unicode 时不会占用更多空间。在这样的编码中,上面提到的保加利亚字符串 (ex1_) 将表示为列表 [208,154,208,176,208,186,208,178,208, 190,32,208,181,32,85,110,105,99,111,100,101,32,63]
,其中第一部分包含保加利亚文字符,每个字符占用更多字节,而尾部部分“Unicode?”与每个列表元素一个字符的普通且更直观的编码相同。
尽管不那么直观,但 UTF-8 编码是操作系统和终端仿真器最广泛传播和支持的编码。因此,UTF-8 是与外部实体(文件、驱动程序、终端等)通信文本的最方便方式。
在 Erlang 中处理列表时,使用每个字符一个列表元素的优点似乎大于在终端上打印之前不必转换 UTF-8 字符串的优点。当目前的 Erlang 实现允许所有当前的 Unicode 字符占用与 latin1 字符相同的内存空间时(请记住,每个字符都表示为一个整数,并且列表元素可以包含最大为 16#7ffffff 的整数,在 32 位实现中,远远大于当前最大的 Unicode 字符 16#10ffff),这一点尤其如此。另一个优点是,像 io:format 这样的例程可以轻松处理 latin1 字符和 Unicode 字符,因为 Unicode 的 8 位字符恰好与 latin1 字符集完全对应。似乎列表有一种非常自然的方式来处理 Unicode 字符。
另一方面,二进制文件将严重受到这样的方案的影响,即每个字符都以能够表示高达 16#10ffff 的数字的固定宽度进行编码。执行此操作的标准化方法通常称为 UTF-32,即每个字符一个 32 位字。即使是 UTF-16 表示形式也会保证使以二进制形式编码的所有文本字符串的内存需求加倍,而 UTF-8 在大多数常见情况下将是最节省空间的表示形式。
二进制文件通常用于表示要发送到外部程序的数据,这也支持 UTF-8 表示形式。
但是,UTF-8 表示形式存在问题,最明显的是字符在二进制文件中占用可变数量的位置(字节),因此遍历会有些繁琐。如果可以在字符串的开头方便地匹配 UTF-8 字符的位语法的扩展将缓解这种情况,但到目前为止,还没有这样的原语。UTF-8 编码的字符也只能向后兼容 7 位 US-ASCII,并且只有概率方法可以确定字节序列是表示编码为 UTF-8 的 Unicode 字符还是普通的 latin1。因此,Erlang 中的库函数需要了解字符在二进制文件中编码的方式才能正确解释它们。如果写入设置为显示 UTF-8 编码的 Unicode 的终端,则高于 128 的 latin1 字符将显示不正确,反之亦然。一个常见的例子是 io:format(“~s~n”,[MyBinaryString]),需要了解该字符串是以 UTF-8 还是 latin1 编码的事实,才能在终端上正确显示。格式化函数实际上提出了关于 Unicode 字符的整套挑战。将需要新的格式化控件来通知 io 和 io_lib 模块中的格式化函数,字符串是 Unicode 格式还是输入是 UTF-8 格式。但是,如下所述,这是可以解决的。
到目前为止,我的结论是,由于二进制文件通常用于节省空间,并且在与外部实体通信时经常使用,因此 UTF-8 的优点似乎优于二进制文件中的缺点。因此,通常将 Unicode 字符以 UTF-8 编码在二进制文件中似乎是明智的。当然,任何表示形式都是可能的,但 UTF-8 将是最常见的情况,因此可以将其视为 Erlang 标准表示形式。
为了进一步使事情复杂化,Erlang 具有 iolist(或 iodata)的概念。io_list 是表示字节序列的整数和二进制文件的任何(或几乎任何)组合,例如 [[85],110,[105,[99]],111,<<100,101>>]
作为字符串 “Unicode” 的表示形式。当将数据发送到驱动程序和许多 BIF 时,接受这种相当方便的表示形式(构造时方便,遍历时不太方便)。
在处理 Unicode 字符串时,需要类似的抽象,并且根据上述建议的约定,这意味着 Unicode 字符串可以是任何组合的列表,其中包含 0 到 16#10ffff 范围内的整数和以 UTF-8 编码的 Unicode 字符的二进制文件。只要知道字符的初始编码方式,就可以轻松地将此类数据转换为纯列表或纯 UTF-8 二进制文件。但是,它不一定是 iolist。此外,转换函数需要知道列表的原始意图才能正确运行。如果想将包含列表部分和二进制部分中 latin1 字符的 iolist 转换为 UTF-8,则不能误解列表部分,因为对于所有 latin1 字符,latin1 和 Unicode 是相同的,但是二进制部分可以,因为如果二进制文件包含 UTF-8 编码的字符,则高于 127 的 latin1 字符以两个字节编码,但使用 latin1 编码时仅使用一个字节。当然,对于其他编码也是如此,如果将 UTF-32 编码的二进制文件转换为 UTF-8,则该过程也将不同于转换 latin1 字符的过程。
如果我们坚持在列表中将 Unicode 表示为每个列表元素一个字符,而在二进制文件中将其表示为 UTF-8 的想法,我们可以有以下定义
系统中已经存在 latin1 列表和 latin1 二进制文件之间以及从混合 latin1 列表到 latin1 二进制文件的转换函数,如 list_to_binary、binary_to_list 和 iolist_to_binary。
可以使用类似于以下函数的函数以类似的方式提供 Unicode 列表、Unicode 二进制文件以及从混合 Unicode 列表的转换
unicode:list_to_utf8(UM) -> Bin
其中 UM 是混合 Unicode 列表,结果是 UTF-8 二进制文件,以及
unicode:utf8_to_list(Bin) -> UL
其中 Bin 是一个由以 UTF-8 编码的 Unicode 字符组成的二进制文件,UL 是 Unicode 字符的纯列表。
为了允许与 latin1 进行转换,以下函数
unicode:latin1_list_to_utf8(LM) -> Bin
和
unicode:latin1_list_to_unicode_list(LM) -> UL
将执行相同的工作。实际上,在此上下文中不需要 latin1_list_to_list,因为它更像是一个 iolist 函数,但为了完整起见,应该存在。
表示 latin1 字符的整数列表是包含 Unicode 字符的列表的子集,但当从混合列表转换为 UTF-8 编码的二进制数据时,这种事实可能会让人感到困惑,而并非有用。我认为一个好的方法是区分处理 latin1 字符和 Unicode 的函数,以便如果二进制数据预期包含 latin1 字节,则混合列表应仅包含 0..255 的数字。对于像 io:format 这样的函数,也应该如此,即 ~s 表示 latin1 混合列表,而 ~ts 表示 Unicode 混合列表(二进制数据为 UTF-8 编码)。如果采用此方法,将大于 255 的整数传递给 ~s 将会出错,就像将相同的内容传递给 latin1_list_to_utf8/1
一样。有关 io 系统的更多讨论,请参见下文。
unicode_list_to_utf8/1
和 latin1_list_to_utf8/1
函数可以合并为单个函数 list_to_utf8/2
,如下所示
unicode:characters_to_binary(ML,InEncoding) -> binary()
ML := A mixed Unicode list or a mixed latin1 list
InEncoding := {latin1 | unicode}
“字符”一词用于表示所关注的编码中字符的可能复杂的表示形式,例如 “以 latin1 表示或 Unicode 表示的字符和/或二进制数据的可能混合且深的列表”的简称。
将编码指定为 latin1 意味着所有 ML 都应被解释为 latin1 字符,这意味着列表中大于 255 的整数将会出错。将编码指定为 Unicode 意味着接受所有 0..16#10ffff 的整数,并且二进制数据应已采用 UTF-8 编码。
同样,可以使用以下函数转换为 Unicode 字符列表
unicode:characters_to_list(ML, InEncoding) -> list()
ML := A mixed Unicode list or a mixed latin1 list
InEncoding := {latin1 | unicode}
我认为使用两个简单的转换函数 characters_to_binary/2 和 characters_to_list/2 的方法很有吸引力,尽管某些输入数据的组合会稍微难以转换(例如,列表中大于 255 的 Unicode 字符与 latin1 中的二进制数据的组合)。扩展位语法以处理 UTF-8 可以轻松编写特殊的转换函数,以处理上述函数无法完成工作的那些罕见情况。
为了适应其他编码,可以扩展 characters_to_binary 功能以处理其他编码。可以使用以下函数提供更通用的功能(最好放在自己的模块中,模块名称 ‘unicode’ 是一个很好的候选名称)
**characters_to_binary(ML) -> binary() | {error, Encoded, Rest} | {incomplete, Encoded, Rest}** |
与 characters_to_binary(ML,unicode,unicode) 相同。
**characters_to_binary(ML,InEncoding) -> binary() | {error, Encoded, Rest} | {incomplete, Encoded, Rest}** |
与 characters_to_binary(ML,InEncoding,unicode) 相同。
**characters_to_binary(ML,InEncoding, OutEncoding) -> binary() | {error, Encoded, Rest} | {incomplete, Encoded, Rest}** |
类型
InEncoding := { latin1 | unicode | utf8 | utf16 | utf32 } |
OutEncoding := { latin1 | unicode | utf8 | utf16 | utf32 } |
选项 ‘unicode’ 是 utf8 的别名,因为这是二进制数据中 Unicode 字符的首选编码。当数据由于输入数据中的错误而无法编码/解码时,将返回错误元组;当输入数据可能正确但被截断时,将返回不完整元组。
**characters_to_list(ML) -> list() | {error, Encoded, Rest} | {incomplete, Encoded, Rest}** |
与 characters_to_list(ML,unicode) 相同。
**characters_to_list(ML,InEncoding) -> list() | {error, Encoded, Rest} | {incomplete, Encoded, Rest}** |
类型
InEncoding := { latin1 | unicode | utf8 | utf16 | utf32 } |
在这里,选项 ‘unicode’ 也表示二进制数据中 utf8 的默认 Erlang 编码,因此是 utf8 的别名。错误元组和不完整元组的返回方式与 characters_to_binary 相同。
请注意,由于成功时返回的数据类型是明确定义的,因此存在保护测试 (is_list/1 和 is_binary/1),因此我建议不要返回笨拙的 {ok, Data} 元组,即使可以返回错误和不完整元组。当已知编码正确时,这使得函数更易于使用,同时仍然可以轻松检查返回值。
通过新类型可以简化在包含 UTF-8 中 Unicode 字符的二进制数据上使用 Erlang 位语法。类型名称 utf8 比 utf-8 更可取,因为破折号 (“-“) 在位语法中具有特殊含义,用于分隔类型、有符号性、字节序和单位。
位语法匹配中的 utf8 类型会将二进制数据中 UTF-8 编码的字符转换为整数,无论它占用多少字节,并将二进制数据的尾部留给与位语法匹配表达式的其余部分进行匹配。
在构造二进制数据时,转换为 UTF-8 的整数因此在生成的二进制数据中可以占用一到四个字节。
由于位语法通常用于解释来自各种外部来源的数据,因此也应该有相应的 utf16 和 utf32 类型。虽然使用当前的位语法实现可以轻松解释 UTF-8、UTF-16 和 UTF-32,但建议的特定类型对于程序员来说会很方便。此外,Unicode 在范围方面施加了限制,并且有一些禁止范围,最好使用内置的位语法类型来处理。
utf16 和 utf32 类型需要具有字节序选项,因为 UTF-16 和 UTF-32 可以存储为大端或小端实体。
给定 Erlang 中的默认 Unicode 字符表示,让我们深入了解格式化函数。我建议使用格式化控制序列修饰符的概念,即在 “~” 和控制字符之间添加一个额外的字符,表示 Unicode 输入/输出。字母 “t”(表示 translate)当前未在任何格式化函数中使用,使其成为一个很好的候选者。修饰符的含义应该是例如格式化控制 “~ts” 表示 Unicode 中的字符串,而 “~s” 表示 iso-latin-1 中的字符串。不简单地引入一个新的单控制字符的原因是,建议的修饰符可以应用于各种控制字符,例如 “p” 甚至 “w”,而新的 Unicode 字符串单控制字符只会替换当前的 “s” 控制字符。
尽管 Erlang 中的 io 协议从一开始就没有对客户端和 io_server 之间可以传输的字符施加任何限制,但 Erlang 中 io 系统的更高性能需求使得后来的实现使用二进制数据进行通信,这实际上使得 io 协议包含字节,而不是通用字符。
此外,io 系统当前使用可以表示为字节的字符这一事实已在众多应用程序中得到利用,因此来自 io 函数(即 io_lib:format)的输出已直接发送到仅接受字节输入(如套接字)的实体,或者已实现 io_server 假设只有 0 - 255 的字符范围。当然,可以更改此设置,但这种更改可能会导致 io 系统性能下降,以及对已在生产环境中的代码进行大量更改(又名“客户代码”)。
Erlang 中的 io 系统目前围绕数据始终是字节流的假设工作。尽管这不是最初的意图,但这就是它今天的用途。这意味着可以将 latin1 字符串以大致相同的方式发送到终端或文件,永远不需要进行任何转换。对于终端来说,情况可能并非总是如此,但在终端的情况下,始终需要进行一次转换,即从字节流转换为终端喜欢的任何格式。磁盘文件是字节流,终端也是如此,至少就 Erlang io 系统而言。此外,io_lib 格式化函数始终返回(可能)深的整数列表,每个整数表示一个字符,这使得难以区分不同的编码。然后,该结果由诸如 io:format 之类的函数按原样发送到 io_server,最终将其放入磁盘。服务器也接受二进制数据,但它们永远不会由 io_lib:format 生成。
当 Erlang 开始支持 Unicode 字符时,世界发生了一点变化。文件可能包含 UTF-8 或 iso-latin-1 中的文本,并且无法从例如 io_lib:format 生成的列表中判断用户最初的意图。
为了尽可能不破坏当前代码,并保留(或恢复)io 系统协议的最初意图,我建议一个方案,其中返回列表的格式化函数尽可能保持当前行为。
因此,如果未使用转换修饰符,则 io_lib:format 函数将返回(可能深的)整数 0..255 的列表(latin1,可以视为 Unicode 的子集)。但是,如果使用转换修饰符,则它将返回完整 Unicode 范围内的整数的可能深度的列表。回到保加利亚字符串 (ex1_),让我们看一下以下内容
1> UniString = [1050,1072,1082,1074,
1086,32,1077,32,85,110,105,99,111,100,101,32,63].
2> io_lib:format("~s",[UniString]).
- 在这里,Unicode 字符串违反了混合 latin1 列表属性,并将引发 badarg 异常。应保留此行为。另一方面
3> io_lib:format("~ts",[UniString]).
- 将返回一个(深的)列表,其中包含 Unicode 字符串作为整数列表
[[1050,1072,1082,1074,1086,32,1077,32,85,110,105,99,111,100,
101,32,63]]
在结果列表中引入大于 255 的整数的缺点当然是该函数的返回值不再是有效的 iodata(),但另一方面,以下代码
lists:flatten(io_lib:format("~ts",[UniString]))
将给出与非 Unicode 版本类似的结果。
由于格式修饰符 “t” 是新的,因此在生成的深层列表中获取大于 255 的整数的可能性不会破坏旧代码。要获取 UTF-8 中的 iodata(),可以简单地执行
unicode:characters_to_binary(io_lib:format("~ts",[UniString]),
unicode, unicode)
和以前一样,直接格式化(使用 ~s)大于 255 的字符列表将会出错,但是使用 “t” 修饰符就可以正常工作。
当涉及到范围检查和向后兼容性时
6> io:format(File,"~s",[UniString]).
- 和以前一样,会抛出 badarg 异常,而
7> io:format(File,"~ts",[UniString]).
- 将会被接受。
io:fread/2,3 的相应行为是希望在此调用中接收 Unicode 数据
11> io:fread(File,'',"~ts").
- 但希望在此调用中接收 latin1 数据
12> io:fread(File,'',"~s").
另一方面,实际的 io 协议应该只处理 Unicode,这意味着当数据被转换为二进制格式进行发送时,所有数据都应被转换为 UTF-8。当在通信中使用整数列表时,latin1 和 Unicode 表示形式是相同的,因此不需要进行转换或施加限制。请记住,io 系统的构建是为了使字符无论在哪个 io 服务器上都应具有相同的解释。唯一可能的编码将是 Unicode 编码。
由于我们在进程之间(io 系统中的客户端和服务器进程)进行大量通信,因此将数据转换为 Unicode 二进制(UTF-8)是处理大量数据的最有效策略。
通常,使用 file 模块写入 io 服务器只能使用面向字节的数据,而使用 io 模块则可以处理 Unicode 字符。调用函数 file:write/2 将按原样将字节发送到文件,因为文件是面向字节的,但是当使用 io 模块写入文件时,期望并处理 Unicode 字符。
当发送到 io 服务器时,io 协议会将字节转换为 Unicode,但如果文件是面向字节的,则转换回来会使用户透明地处理此过程。所有字节都可以在 UTF-8 中表示,并且可以轻松地来回转换。
不兼容的更改将是对 io 中的 put_chars 函数进行的。它应该只允许 Unicode 数据,而不是像现在文档中记录的那样允许 iodata()。最大的变化是,提供给该函数的任何二进制文件都必须是 UTF-8 格式。但是,此函数的大多数用法都限于列表,因此预计这种不兼容的更改不会给用户带来麻烦。
为了处理文件上可能的 Unicode 文本数据,应该能够在打开文件时提供编码参数。默认情况下,文件应以字节(或 latin1)编码打开,同时应提供打开文件以进行例如 utf8 转换的选项。
让我们看一些示例
像往常一样使用 file:open 打开文件。然后我们想向其中写入字节
使用带有 iodata()(字节)的 file:write,数据由 io 协议转换为 UTF-8,但是 io 服务器会在实际将字节写入文件之前将其转换回 latin1。为了获得更好的性能,可以以原始模式打开文件,从而避免所有转换。
使用 file:write 写入用户已经转换为 UTF-8 的数据,io 协议会将其嵌入到另一层 UTF-8 编码中,文件服务器将解包它,我们最终会得到按预期写入文件的 UTF-8 字节。
使用 io:put_chars,如果发送的任何 Unicode 字符无法用一个字节表示,则 io 服务器将返回错误。但是,即使它们可能在发送到 io:put_chars 的二进制文件中被编码为 UTF-8,latin1 中可表示的字符也会被正确写入。只要使用 io_lib:format 函数时不使用翻译修饰符,一切都将是有效的 latin1,并且所有返回值都将是列表,因此它是有效的 Unicode *并且* 可以写入默认文件。旧代码将像以前一样运行,除非向 io:put_chars 提供 latin1 二进制文件,在这种情况下,应将调用替换为 file:write 调用。
使用一个参数打开文件,该参数说明应以定义的编码写入 Unicode 数据,在这种情况下,我们将选择 UTF-16/bigendian 以避免与本机 UTF-8 编码混淆。我们使用 file:open(Name,[write,{encoding,utf16,bigendian}]) 打开文件。
使用带有 iodata() 的 file:write,io 协议将转换为默认的 Unicode 表示形式 (UTF-8) 并将数据发送到 io 服务器,io 服务器又将数据转换为 UTF-16 并将其写入文件。该文件将被视为文本文件,发送给它的所有 iodata() 都将被视为文本。
如果数据已经采用 Unicode 表示形式(例如 UTF-8),则不应使用 file:write 将其写入此类型的文件,预计应使用 io:put_chars(这不是问题,因为旧代码中不应存在 Unicode 数据,这仅当打开文件进行翻译时才是一个问题)。
如果数据采用 Erlang 默认的 Unicode 格式,则可以使用 io:put_chars 将其写入文件。这适用于所有类型的整数列表和 UTF-8 格式的二进制文件,对于其他表示形式(最明显的是二进制文件中的 latin1),应在使用之前使用 Unicode:characters_to_XXX(Data,latin1) 转换数据。对于 latin1 混合列表 (iodata()),也可以直接使用 file:write。
总结这种情况 - Unicode 字符串(包括 latin1 列表)使用 io:put_chars 写入转换文件,但是纯 iodata() 也可以通过使用 file:write 隐式转换为编码。
为原始访问打开的文件将仅处理字节,它不能与 io:put_chars 一起使用。
使用 io_lib:format 格式化的数据仍然可以使用 file:write 写入原始文件。数据最终将按原样写入。如果在格式化时始终使用翻译修饰符,则文件将获得本机 UTF-8 编码,如果没有使用翻译修饰符,则文件将具有 latin1 编码(从 io_lib:format 返回的列表中每个字符都将可以表示为一个 latin1 字节)。如果数据以不同的方式生成,则必须使用转换函数。
使用 file:write 写入的数据将直接写入文件,不会发生与 Unicode 表示形式之间的转换。
当打开文件进行读取时,与写入时应用的事情大致相同。
任何文件上的 file:read 都期望 io 协议以 Unicode 形式传递数据。每个字节都将由 io 服务器转换为 Unicode,然后由 file:read 转换回字节
如果文件实际上包含 Unicode 字符,它们将被逐字节转换为 Unicode,然后再转换回来,从而使 file:read 获得原始编码。如果读取为(或转换为)二进制文件,则可以使用转换例程轻松地将其转换回 Erlang 默认表示形式。
如果使用 io:get_chars 读取文件,则所有字符将按预期以列表形式返回。所有字符都将是 latin1,但这是 Unicode 的一个子集,并且与读取转换文件没有区别。但是,如果该文件包含 Unicode 转换的字符并以这种方式读取,则来自 io:get_chars 的返回值将很难解释,但这在意料之中。如果需要此功能,可以使用 list_to_binary 将列表转换为二进制文件,然后将其作为该文件实际具有的编码中的 Unicode 实体进行探索。
与写入时一样,读取 Unicode 转换文件最好使用 io 模块。让我们再次假设文件上使用了 UTF-16。
当使用 file:read 读取时,UTF-16 数据将被转换为 Erlang 原生的 Unicode 表示形式并发送到客户端。如果客户端使用 file:read,它将以与写入时字节转换为 Unicode 协议的方式相同的方式将数据转换回字节。如果一切都可以用字节表示,该函数将成功,但是如果存在任何大于 255 的 Unicode 字符,该函数将因解码错误而失败。
不能通过使用 file 模块检索代码点超过 255 范围内的 Unicode 数据。应改用 io 模块。
io:get_chars 和 io:get_line 将处理 io 协议提供的 Unicode 数据。所有 Unicode 返回值都将按预期为 Unicode 列表。仅当提供翻译修饰符时,fread 函数才会返回具有整数 > 255 的列表。
与写入一样,只能使用 file 模块,并且只能读取面向字节的数据。如果编码,则在读取和写入原始文件时,该编码将保留。
通过此解决方案,file 模块与 latin1 io 服务器(又名通用文件)和原始文件一致。添加了一个文件类型,即转换文件,以便 io 模块能够隐式转换其 Unicode 数据(另一个具有隐式转换的 io 服务器示例当然是终端)。从接口的角度来看,通用文件的行为与以前相同,我们只获得了添加的功能。
缺点是 io:put_chars 的行为略有变化,以及当在非原始文件上使用默认 (latin1/byte) 编码的 file 模块时,转换为 Unicode 表示形式以及从 Unicode 表示形式转换的性能影响。可以通过扩展 io 协议以将整个数据块标记为字节 (latin1) 或 Unicode 来更改后者,但是在这些情况下,使用原始文件写入大量数据通常是更好的解决方案。
我建议列表中的 Unicode 表示形式的约定是每个元素一个字符,二进制文件中为 UTF-8,混合 Unicode 实体是这些的组合。
我还建议使用一个模块“unicode”,其中包含用于在 Unicode 的表示形式之间进行转换的函数。所有函数的默认格式应为二进制文件中的 utf8,以指出这是二进制文件中 Unicode 字符的首选内部表示形式。
两个主要的转换函数应为 characters_to_binary/3 和 characters_to_list/2,如上所述。
我建议扩展位语法,允许以 UTF-8 编码进行匹配和构造,例如
<<Ch/utf8,_/binary>> = BinString
以及
MyBin = <<Ch/utf8,More/binary>>
可选地,可以为二进制文件以类似的方式支持 UTF-16,例如
<<Ch/utf16-little,_/binary>> = BinString
UTF-32 将需要以类似于 UTF-16 的方式支持,既是为了完整性,也是为了转换 Unicode 字符时涉及的范围检查。
最后,我建议使用“t”修饰符来控制格式化函数中的序列,该函数期望混合使用整数 0..16#10ffff 的列表和具有 UTF-8 编码的 Unicode 字符的二进制文件。io 和 io_lib 中的函数将保留其当前功能,以用于未使用翻译修饰符的代码,但是当被命令时将返回 Unicode 字符。
fread 函数应以相同的方式仅在使用了“t”修饰符时才接受 Unicode 数据。
io 协议需要更改为始终处理 Unicode 字符。打开文件时提供的选项将允许隐式转换文本文件。
本文档已置于公共领域。