UTF-16

本页使用了标题或全文手工转换,现处于澳门繁体模式
求聞百科,共筆求聞

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>
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. 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

外部連結