此 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 是一種字元編碼標準,其中所有已知的、現存的和歷史上的書寫語言都以單一字元集表示,這當然會導致字元需要超過八位元才能表示。
無論表示方式為何,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 函式,但為了完整性應該存在。
然而,當從混合列表轉換為 UTF-8 編碼的二進制時,代表 latin1 字元的整數列表是包含 Unicode 字元的列表的子集這一事實,可能反而會造成混淆而不是有用。我認為一個好的方法是區分處理 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}
「字元 (characters)」一詞用於表示所涉及編碼中字元的可能複雜表示形式,例如「以 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」(代表翻譯)目前沒有在任何格式化函式中使用,這使其成為一個很好的候選字元。修飾符的含義應該是例如格式化控制「~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。為了獲得更好的效能,可以以原始模式開啟檔案,避免所有轉換。
使用使用者已轉換為 UTF-8 的資料的 file:write,io 協定會將其嵌入另一個 UTF-8 編碼層中,檔案伺服器會解壓縮它,而我們最終會將 UTF-8 位元組寫入檔案中,如同預期的一樣。
使用 io:put_chars,如果傳送的任何 Unicode 字元無法以一個位元組表示,io 伺服器會傳回錯誤。但是,即使它們可能在傳送至 io:put_chars 的二進制檔中編碼為 UTF-8,以 latin1 表示的字元也會順利寫入。只要使用沒有轉換修飾符的 io_lib:format 函式,所有內容都會是有效的 latin1,而且所有傳回值都會是列表,因此它是有效的 Unicode *而且* 可以寫入預設檔案。舊程式碼的功能和以前一樣,除非以 latin1 二進制檔饋入 io:put_chars,否則在這種情況下,呼叫應替換為 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_server 轉換為 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/位元組) 編碼的非原始檔案上使用 file 模組時,來回轉換為 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 字元。在開啟檔案時提供的選項將允許對文字檔案進行隱式轉換。
本文檔已置於公共領域。