UTF-16
UTF-16是Unicode字元編碼五層次模型的第三層:字元編碼表(Character Encoding Form,也稱為"storage format")的一種實現方式。即把Unicode字元集的抽象碼位對映為16位元長的整數(即碼元)的序列,用於數據儲存或傳遞。Unicode字元的碼位,需要1個或者2個16位元長的碼元來表示,因此這是一個變長表示。
UTF是"Unicode/UCS Transformation Format"的首字母縮寫,即把Unicode字元轉換為某種格式之意。UTF-16正式定義於ISO/IEC 10646-1的附錄C,而RFC2781也定義了相似的做法。
UTF-16描述
Unicode的編碼空間從U+0000到U+10FFFF,共有1,112,064個碼位(code point)可用來對映字元。Unicode的編碼空間可以劃分為17個平面(plane),每個平面包含216(65,536)個碼位。17個平面的碼位可表示為從U+xx0000到U+xxFFFF,其中xx表示十六進制值從0016到1016,共計17個平面。第一個平面稱為基本多語言平面(Basic Multilingual Plane, BMP),或稱第零平面(Plane 0),其他平面稱為輔助平面(Supplementary Planes)。基本多語言平面內,從U+D800到U+DFFF之間的碼位區段是永久保留不對映到Unicode字元。UTF-16就利用保留下來的0xD800-0xDFFF區段的碼位來對輔助平面的字元的碼位進行編碼。
從U+0000至U+D7FF以及從U+E000至U+FFFF的碼位
第一個Unicode平面(碼位從U+0000至U+FFFF)包含了最常用的字元。該平面被稱為基本多語言平面,縮寫為BMP(Basic Multilingual Plane,BMP)。UTF-16與UCS-2編碼這個範圍內的碼位為16位元長的單個碼元,數值等價於對應的碼位。BMP中的這些碼位是僅有的可以在UCS-2中表示的碼位。
從U+10000到U+10FFFF的碼位
輔助平面(Supplementary Planes)中的碼位,在UTF-16中被編碼為一對16位元長的碼元(即32位元,4位元組),稱作代理對(Surrogate Pair),具體方法是:
lead \ trail | DC00 | DC01 | … | DFFF |
---|---|---|---|---|
D800 | 10000 | 10001 | … | 103FF |
D801 | 10400 | 10401 | … | 107FF |
⋮ | ⋮ | ⋮ | ⋱ | ⋮ |
DBFF | 10FC00 | 10FC01 | … | 10FFFF |
- 碼位減去
0x10000
,得到的值的範圍為20位元長的0...0xFFFFF
。 - 高位的10位元的值(值的範圍為
0...0x3FF
)被加上0xD800
得到第一個碼元或稱作高位代理(high surrogate),值的範圍是0xD800...0xDBFF
。由於高位代理比低位代理的值要小,所以為了避免混淆使用,Unicode標準現在稱高位代理為前導代理(lead surrogates)。 - 低位的10位元的值(值的範圍也是
0...0x3FF
)被加上0xDC00
得到第二個碼元或稱作低位代理(low surrogate),現在值的範圍是0xDC00...0xDFFF
。由於低位代理比高位代理的值要大,所以為了避免混淆使用,Unicode標準現在稱低位代理為後尾代理(trail surrogates)。
上述演算法可理解為:輔助平面中的碼位從U+10000到U+10FFFF,共計FFFFF個,即220=1,048,576個,需要20位來表示。如果用兩個16位元長的整陣列成的序列來表示,第一個整數(稱為前導代理)要容納上述20位的前10位,第二個整數(稱為後尾代理)容納上述20位的後10位。還要能根據16位元整數的值直接判明屬於前導整數代理的值的範圍(210=1024),還是後尾整數代理的值的範圍(也是210=1024)。因此,需要在基本多語言平面中保留不對應於Unicode字元的2048個碼位,就足以容納前導代理與後尾代理所需要的編碼空間。這對於基本多語言平面總計65536個碼位來說,僅佔3.125%。
由於前導代理、後尾代理、BMP中的有效字元的碼位,三者互不重疊,搜尋是簡單的:一個字元編碼的一部分不可能與另一個字元編碼的不同部分相重疊。這意味着UTF-16是自同步(self-synchronizing)的:可以通過僅檢查一個碼元來判定給定字元的下一個字元的起始碼元。UTF-8也有類似優點,但許多早期的編碼模式就不是這樣,必須從頭開始分析文字才能確定不同字元的碼元的邊界。
由於最常有的字元都在基本多文種平面中,許多軟件處理代理對的部分往往得不到充分的測試。這導致了一些長期的bug與潛在安全漏洞,它們甚至存在於廣為流行且評價頗高的應用軟件中[1]。
從U+D800到U+DFFF的碼位
Unicode標準規定U+D800...U+DFFF的值不對應於任何字元。
但是在使用UCS-2的時代,U+D800...U+DFFF內的值被佔用,用於某些字元的對映。但只要不構成代理對,許多UTF-16編碼解碼還是能把這些不符合Unicode標準的字元對映正確的辨識、轉換成合規的碼元[2]。按照Unicode標準,這種碼元序列本來應算作編碼錯誤。
範例:
以U+10437編碼(𐐷)為例:
0x10437
減去0x10000
,結果為0x00437
,二進制為0000 0000 0100 0011 0111
- 分割它的上10位值和下10位值(使用二進制):
0000 0000 01
和00 0011 0111
- 添加
0xD800
到上值,以形成高位:0xD800 + 0x0001 = 0xD801
- 添加
0xDC00
到下值,以形成低位:0xDC00 + 0x0037 = 0xDC37
- 下表總結了一起範例的轉換過程,顏色指示碼點位如何分佈在所述的UTF-16中。由UTF-16編碼過程中加入附加位的以黑色顯示。
字元 | 普通二進制 | UTF-16二進制 | UTF-16 十六進制 字元代碼 |
UTF-16BE 十六進制位元組 |
UTF-16LE 十六進制位元組 | |
---|---|---|---|---|---|---|
$ | U+0024
|
0000 0000 0010 0100
|
0000 0000 0010 0100
|
0024
|
00 24
|
24 00
|
€ | U+20AC
|
0010 0000 1010 1100
|
0010 0000 1010 1100
|
20AC
|
20 AC
|
AC 20
|
𐐷 | U+10437
|
0001 0000 0100 0011 0111
|
1101 1000 0000 0001 1101 1100 0011 0111
|
D801 DC37
|
D8 01 DC 37
|
01 D8 37 DC
|
𤭢 | U+24B62
|
0010 0100 1011 0110 0010
|
1101 1000 0101 0010 1101 1111 0110 0010
|
D852 DF62
|
D8 52 DF 62
|
52 D8 62 DF
|
範例:UTF-16編碼程式
假設要將U+64321(16進位)轉成UTF-16編碼。因為它超過U+FFFF,所以他必須編譯成32位元(4個byte)的格式,如下所示:
V = 0x64321 Vx = V - 0x10000 = 0x54321 = 0101 0100 0011 0010 0001 Vh = 01 0101 0000 // Vx的高位部份的10 bits Vl = 11 0010 0001 // Vx的低位部份的10 bits w1 = 0xD800 //結果的前16位元初始值 w2 = 0xDC00 //結果的後16位元初始值 w1 = w1 | Vh = 1101 1000 0000 0000 | 01 0101 0000 = 1101 1001 0101 0000 = 0xD950 w2 = w2 | Vl = 1101 1100 0000 0000 | 11 0010 0001 = 1101 1111 0010 0001 = 0xDF21
所以這個字U+64321最後正確的UTF-16編碼應該是:
0xD950 0xDF21
而在小尾序中最後的編碼應該是:
0x50D9 0x21DF
因為這個字超過U+FFFF所以無法用UCS-2的格式編碼。
16進制編碼範圍 | UTF-16表示方法(二進制) | 10進制碼範圍 | 位元組數量 |
---|---|---|---|
U+0000 - U+FFFF |
xxxx xxxx xxxx xxxx - yyyy yyyy yyyy yyyy |
0-65535 | 2 |
U+10000 - U+10FFFF |
1101 10yy yyyy yyyy - 1101 11xx xxxx xxxx |
65536-1114111 | 4 |
UTF-16比起UTF-8,好處在於大部分字元都以固定長度的位元組(2位元組)儲存,但UTF-16卻無法相容於ASCII編碼。
UTF-16的編碼模式
UTF-16的大尾序和小尾序儲存形式都在用。一般來說,以Macintosh製作或儲存的文字使用大尾序格式,以Microsoft或Linux製作或儲存的文字使用小尾序格式。
為了弄清楚UTF-16檔案的大小尾序,在UTF-16檔案的開首,都會放置一個U+FEFF字元作為Byte Order Mark(UTF-16 LE以 FF FE
代表,UTF-16 BE以 FE FF
代表),以顯示這個文字檔案是以UTF-16編碼,其中U+FEFF字元在UNICODE中代表的意義是 ZERO WIDTH NO-BREAK SPACE
,顧名思義,它是個沒有寬度也沒有斷字的空白。
以下的例子有四個字元:「朱」(U+6731)、半形逗號(U+002C)、「聿」(U+807F)、「𪚥」(U+2A6A5)。
使用UTF-16編碼的例子 | ||||||
---|---|---|---|---|---|---|
編碼名稱 | 編碼次序 | 編碼 | ||||
BOM | 朱 | , | 聿 | 𪚥 | ||
UTF-16 LE | 小尾序,不含BOM | 31 67 | 2C 00 | 7F 80 | 69 D8 A5 DE | |
UTF-16 BE | 大尾序,不含BOM | 67 31 | 00 2C | 80 7F | D8 69 DE A5 | |
UTF-16 LE | 小尾序,包含BOM | FF FE | 31 67 | 2C 00 | 7F 80 | 69 D8 A5 DE |
UTF-16 BE | 大尾序,包含BOM | FE FF | 67 31 | 00 2C | 80 7F | D8 69 DE A5 |
UTF-16與UCS-2的關係
UTF-16可看成是UCS-2的父集。在沒有輔助平面字元(surrogate code points)前,UTF-16與UCS-2所指的是同一的意思。但當引入輔助平面字元後,就稱為UTF-16了。現在若有軟件聲稱自己支援UCS-2編碼,那其實是暗指它不能支援在UTF-16中超過2位元組的字集。對於小於0x10000的UCS碼,UTF-16編碼就等於UCS碼。
Microsoft Windows作業系統內核對Unicode的支援
Windows作業系統內核中的字元表示為UTF-16小尾序,可以正確處理、顯示以4位元組儲存的字元。但是Windows API實際上僅能正確處理UCS-2字元,即僅以2位元組儲存的,碼位小於U+FFFF的Unicode字元。其根源是Microsoft C++語言把 wchar_t
資料類型定義為16位元的unsigned short,這就與一個 wchar_t
型變數對應一個寬字元、可以儲存一個Unicode字元的規定相矛盾。相反,Linux平台的GCC編譯器規定一個 wchar_t
是4位元組長度,可以儲存一個UTF-32字元,寧可浪費了很大的儲存空間。下例執行於Windows平台的C++程式可說明此點:
// 此源文件在Windows平台上必须保存为Unicode格式(即UTF-16小尾)
// 因为包含的汉字“𪚥”,不能在简体中文版Windows默认的代码页936(即GBK)中表示
// 该汉字在UTF-16小尾序中用4个字节表示
// Windows操作系统能正确显示这样的在UTF-16需用4字节表示的字符
// 但是Windows API不能正确处理这样的在UTF-16需用4字节表示的字符,把它判定为2个UCS-2字符
#include <windows.h>
#include <stdio.h>
int main()
{
const wchar_t lwc[] = L"𪚥";
MessageBoxW(NULL, lwc, lwc, MB_OK);
int i = wcslen(lwc);
printf("%d\n", i);
int j = lstrlenW(lwc);
printf("%d\n", j);
return 0;
}
Windows 9x系統的API僅支援ANSI字元集,只支援部分的UCS-2轉換。1996年發佈的Windows NT 4.0的API支援UCS-2。Windows 2000開始,Windows系統API開始支援UTF-16,並支援Surrogate Pair;但許多系統控制項比如文本框和label等還不支援surrogate pair表示的字元,會顯示成兩個字元。Windows 7及更新的系統已經良好地支援了UTF-16,包括Surrogate Pair。
Windows API支援在UTF-16LE(wchar_t
類型)與UTF-8(頁碼CP_UTF8)之間的轉碼。例如:
#include <windows.h>
int main() {
char a1[128], a2[128] = { "Hello" };
wchar_t w = L'页';
int n1, n2= 5;
wchar_t w1[128];
int m1 = 0;
n1 = WideCharToMultiByte(CP_UTF8, 0, &w, 1, a1, 128, NULL, NULL);
m1 = MultiByteToWideChar(CP_UTF8, 0, a2, n2, w1, 128);
}
參考文獻
- ^ Code in Apache Xalan 2.7.0 which can fail on surrogate pairs. Apache Foundation. [2012-03-23]. (原始內容存檔於2011-04-23).
The code wrongly assumes it is safe to use
substring
on the input - ^ Python 2.6 decode of UTF16 does this on Linux, and it correctly handles surrogate pairs. All "CESU" decoders do it too, though they also mistranslate correct surrogate pairs into 2 characters
外部連結
- Unicode Technical Note #12: UTF-16 for Processing (頁面存檔備份,存於互聯網檔案館)
- A very short algorithm for determining the surrogate pair for any codepoint(頁面存檔備份,存於互聯網檔案館)
- Unicode FAQ: What is the difference between UCS-2 and UTF-16? (頁面存檔備份,存於互聯網檔案館)
- Unicode Character Name Index (頁面存檔備份,存於互聯網檔案館)
- RFC 2781: UTF-16, an encoding of ISO 10646
- java.lang.String documentation, discussing surrogate handling (頁面存檔備份,存於互聯網檔案館)