UTF-16Unicode字元編碼五層次模型的第三層:字元編碼表(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),具體方法是:

UTF-16解碼
lead \ trail DC00 DC01    …    DFFF
D800 10000 10001 103FF
D801 10400 10401 107FF
  ⋮
DBFF 10FC00 10FC01 10FFFF
  1. 碼位減去 0x10000,得到的值的範圍為20位元長的 0...0xFFFFF
  2. 高位的10位元的值(值的範圍為 0...0x3FF)被加上 0xD800 得到第一個碼元或稱作高位代理(high surrogate),值的範圍是 0xD800...0xDBFF。由於高位代理比低位代理的值要小,所以為了避免混淆使用,Unicode標準現在稱高位代理為前導代理(lead surrogates)。
  3. 低位的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編碼(𐐷)為例:

  1. 0x10437 減去 0x10000,結果為0x00437,二進制為 0000 0000 0100 0011 0111
  2. 分割它的上10位值和下10位值(使用二進制):0000 0000 0100 0011 0111
  3. 添加 0xD800 到上值,以形成高位0xD800 + 0x0001 = 0xD801
  4. 添加 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製作或儲存的文字使用大尾序格式,以MicrosoftLinux製作或儲存的文字使用小尾序格式。

為了弄清楚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);
}

參考文獻

  1. ^ 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 
  2. ^ 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

外部連結