1. 程式人生 > >UTF-16 -- 頂級程式設計師也會忽略的系統編碼問題,JDK 錯了十年!

UTF-16 -- 頂級程式設計師也會忽略的系統編碼問題,JDK 錯了十年!

   Unicode(統一碼、萬國碼、單一碼)是電腦科學領域裡的一項業界標準,包括字符集、編碼方案等。Unicode 是為了解決傳統的字元編碼方案的侷限而產生的,它為每種語言中的每個字元設定了統一併且唯一的二進位制編碼,以滿足跨語言、跨平臺進行文字轉換、處理的要求。

 

  Unicode是國際組織制定的可以容納世界上所有文字和符號的字元編碼方案。目前的Unicode字元分為 17 組編排,0x0000 至 0x10FFFF,每組稱為平面(Plane),而每平面擁有65536個碼位,共1114112個。然而目前只用了少數平面。UTF-8、UTF-16、UTF-32 都是將數字轉換到程式資料的編碼方案。

 

  UTF-16 是 Unicode字元編碼五層次模型的第三層:字元編碼表的一種實現方式。即把 Unicode 字符集的抽象碼位對映為 16 位長的整數的序列 (即碼元),用於資料儲存或傳遞。Unicode字元的碼位,需要1個或者 2 個 16 位長的碼元來表示,因此這是一個變長編碼。

 

  上為 UTF-16 編碼的介紹,紅字部分強調它是一個變長編碼;但實際上很多對編碼有理解的人也都以為它是固定長度編碼。這個錯誤的認知在日常程式設計中似乎不會有什麼問題,因為常用中文字元都可以用 1 個 16 位長的碼元表示。但是,中文有近 8 萬個字元,而 1 個 16 位長的碼元最大值僅是 65535(0xffff),所以超過一半的不常用中文字元被定義為擴充套件字元,這些字元需要用 2 個 16 位長的碼元表示。

 

  UTF-16 編碼以16位無符號整數為單位。我們把 Unicode 編碼記作 U。編碼規則如下:

 

  如果 U < 0x10000,U 的 UTF-16 編碼就是 U 對應的16位無符號整數(為書寫簡便,下文將16位無符號整數記作WORD)。

 

  如果 U >= 0x10000,我們先計算 U' = U - 0x10000,然後將 U' 寫成二進位制形式:yyyy yyyy yyxx xxxx xxxx,U 的 UTF-16 編碼(二進位制)就是:110110yyyyyyyyyy 110111xxxxxxxxxx。

 

  為什麼 U' 可以被寫成 20 個二進位制位?Unicode 的最大碼位是 0x10FFFF,減去 0x10000 後,U' 的最大值是0xFFFFF,所以肯定可以用 20 個二進位制位表示。例如:Unicode 編碼 0x20C30,減去 0x10000 後,得到 0x10C30,寫成二進位制是:0001 0000 1100 0011 0000。用前10位依次替代模板中的 y,用後 10 位依次替代模板中的 x,就得到:1101100001000011 1101110000110000,即 0xD843 0xDC30。

 

  按照上述規則,Unicode 編碼 0x10000-0x10FFFF 的 UTF-16 編碼有兩個 WORD,第一個 WORD 的高 6 位是 110110,第二個 WORD 的高 6 位是 110111。可見,第一個 WORD 的取值範圍(二進位制)是 11011000 00000000到 11011011 11111111,即 0xD800-0xDBFF。第二個 WORD 的取值範圍(二進位制)是 11011100 00000000到 11011111 11111111,即 0xDC00-0xDFFF。

 

  為了將一個 WORD 的 UTF-16 編碼與兩 個WORD 的 UTF-16 編碼區分開來,Unicode 編碼的設計者將0xD800-0xDFFF 保留下來,並稱為代理區(Surrogate):

D800-DBFF High Surrogates 高位替代
DC00-DFFF Low Surrogates 低位替代
 

 

 

 

  高位替代就是指這個範圍的碼位是兩個WORD的UTF-16編碼的第一個WORD。低位替代就是指這個範圍的碼位是兩個WORD的UTF-16編碼的第二個WORD。

 

  上述是對 UTF-16 編碼規則的說明,那如何實現它呢?下面使用 C# 程式碼演示如何 UTF-16 與 UTF-32 間互轉:

    public class Demo
    {
        internal const char HIGH_SURROGATE_START = '\ud800';
        internal const char HIGH_SURROGATE_END = '\udbff';
        internal const char LOW_SURROGATE_START = '\udc00';
        internal const char LOW_SURROGATE_END = '\udfff';

        internal const int UNICODE_PLANE00_END = 0x00ffff;
        internal const int UNICODE_PLANE01_START = 0x10000;
        internal const int UNICODE_PLANE16_END = 0x10ffff;

        public static bool IsHighSurrogate(char c)
        {
            return ((c >= HIGH_SURROGATE_START) && (c <= HIGH_SURROGATE_END));
        }

        public static bool IsLowSurrogate(char c)
        {
            return ((c >= LOW_SURROGATE_START) && (c <= LOW_SURROGATE_END));
        }

        public static char[] ConvertFromUtf32(int utf32)
        {
            // U+00D800 ~ U+00DFFF 這個範圍被 Unicode 定義為專用代理區,它們不能作為 Unicode 編碼值。
            if ((utf32 < 0 || utf32 > UNICODE_PLANE16_END) || (utf32 >= HIGH_SURROGATE_START && utf32 <= LOW_SURROGATE_END))
            {
                throw new ArgumentOutOfRangeException("utf32");
            }

            if (utf32 < UNICODE_PLANE01_START)
            {
                // 這是一個基本字元。
                return new char[] { (char)utf32 };
            }

            // 這是一個擴充套件字元,需要將其轉換為 UTF-16 中的代理項對。
            utf32 -= UNICODE_PLANE01_START;

            return new char[]
            {
                (char)((utf32 / 0x400) + HIGH_SURROGATE_START),
                (char)((utf32 % 0x400) + LOW_SURROGATE_START)
            };
        }

        public static int ConvertToUtf32(char highSurrogate, char lowSurrogate)
        {
            if (!IsHighSurrogate(highSurrogate))
            {
                throw new ArgumentOutOfRangeException("highSurrogate");
            }
            if (!IsLowSurrogate(lowSurrogate))
            {
                throw new ArgumentOutOfRangeException("lowSurrogate");
            }

            return (((highSurrogate - HIGH_SURROGATE_START) * 0x400) + (lowSurrogate - LOW_SURROGATE_START) + UNICODE_PLANE01_START);
        }
    }

  為何說 JDK 在這一方面錯了十年呢?因為在 Java 7 時期,因為字串架構設計不合理,誤 utf-16 將以為是固定長度編碼,而實際 utf-16 是可變長度編碼,因為 char(word) 的最大值是 0xffff,而 Unicode 規範最大值是 0x10ffff,小概率出現的字元需要兩個 char 才能表示。Java 後來意識到這個錯誤,並 Java 接下來的幾個版本里,匆匆將字串編碼改為 utf8(實際是,判斷如果有字元超出 0xffff,則使用 utf-8,否則還是繼續 不正常的 utf-16 演算法)。再後來 Java 才使用上了正常的 utf-16 編碼。

 

 

  前兩年有個段子,說只有 2000 元以上的手機才能在輸入法打出某個漢字。原因正是這個。       這裡附上由我的開源專案:.Net 平臺的高效能 Json 解析庫:Swifter.Json:https://github.com/Dogwei/Swifter.Json。希望大家支援一下。     最後附上 Swifter.Json 內部使用的 Utf16 與 Utf8 互轉的原始碼:
    public static unsafe class EncodingHelper
    {
        public const char ASCIIMaxChar = (char)0x7f;
        public const int Utf8MaxBytesCount = 4;

        public static int GetUtf8Bytes(char* chars, int length, byte* bytes)
        {
            var destination = bytes;

            for (int i = 0; i < length; i++)
            {
                int c = chars[i];

                if (c <= 0x7f)
                {
                    *destination = (byte)c; ++destination;
                }
                else if (c <= 0x7ff)
                {
                    *destination = (byte)(0xc0 | (c >> 6)); ++destination;
                    *destination = (byte)(0x80 | (c & 0x3f)); ++destination;
                }
                else if (c >= 0xd800 && c <= 0xdbff)
                {
                    c = ((c & 0x3ff) << 10) + 0x10000;

                    ++i;

                    if (i < length)
                    {
                        c |= chars[i] & 0x3ff;
                    }

                    *destination = (byte)(0xf0 | (c >> 18)); ++destination;
                    *destination = (byte)(0x80 | ((c >> 12) & 0x3f)); ++destination;
                    *destination = (byte)(0x80 | ((c >> 6) & 0x3f)); ++destination;
                    *destination = (byte)(0x80 | (c & 0x3f)); ++destination;
                }
                else
                {
                    *destination = (byte)(0xe0 | (c >> 12)); ++destination;
                    *destination = (byte)(0x80 | ((c >> 6) & 0x3f)); ++destination;
                    *destination = (byte)(0x80 | (c & 0x3f)); ++destination;
                }
            }

            return (int)(destination - bytes);
        }

        [MethodImpl(VersionDifferences.AggressiveInlining)]
        public static int GetUtf8Chars(byte* bytes, int length, char* chars)
        {
            var destination = chars;

            var current = bytes;
            var end = bytes + length;

            for (; current < end; ++current)
            {
                if ((*((byte*)destination) = *current) > 0x7f)
                {
                    return GetGetUtf8Chars(current, end, destination, chars);
                }

                ++destination;
            }

            return (int)(destination - chars);
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private static int GetGetUtf8Chars(byte* current, byte* end, char* destination, char* chars)
        {
            if (current + Utf8MaxBytesCount < end)
            {
                end -= Utf8MaxBytesCount;

                // Unchecked index.
                for (; current < end; ++current)
                {
                    var byt = *current;

                    if (byt <= 0x7f)
                    {
                        *destination = (char)byt;
                    }
                    else if (byt <= 0xdf)
                    {
                        *destination = (char)(((byt & 0x1f) << 6) | (current[1] & 0x3f));

                        ++current;
                    }
                    else if (byt <= 0xef)
                    {
                        *destination = (char)(((byt & 0xf) << 12) | ((current[1] & 0x3f) << 6) + (current[2] & 0x3f));

                        current += 2;
                    }
                    else
                    {
                        var utf32 = (((byt & 0x7) << 18) | ((current[1] & 0x3f) << 12) | ((current[2] & 0x3f) << 6) + (current[3] & 0x3f)) - 0x10000;

                        *destination = (char)(0xd800 | (utf32 >> 10)); ++destination;
                        *destination = (char)(0xdc00 | (utf32 & 0x3ff));

                        current += 3;
                    }

                    ++destination;
                }

                end += Utf8MaxBytesCount;
            }

            // Checked index.
            for (; current < end; ++current)
            {
                var byt = *current;

                if (byt <= 0x7f)
                {
                    *destination = (char)byt;
                }
                else if (byt <= 0xdf && current + 1 < end)
                {
                    *destination = (char)(((byt & 0x1f) << 6) | (current[1] & 0x3f));

                    ++current;
                }
                else if (byt <= 0xef && current + 2 < end)
                {
                    *destination = (char)(((byt & 0xf) << 12) | ((current[1] & 0x3f) << 6) + (current[2] & 0x3f));

                    current += 2;
                }
                else if (current + 3 < end)
                {
                    var utf32 = (((byt & 0x7) << 18) | ((current[1] & 0x3f) << 12) | ((current[2] & 0x3f) << 6) + (current[3] & 0x3f)) - 0x10000;

                    *destination = (char)(0xd800 | (utf32 >> 10)); ++destination;
                    *destination = (char)(0xdc00 | (utf32 & 0x3ff));

                    current += 3;
                }

                ++destination;
            }

            return (int)(destination - chars);
        }

        public static int GetUtf8CharsLength(byte* bytes, int length)
        {
            int count = 0;

            for (int i = 0; i < length; i += bytes[i] <= 0x7f ? 1 : bytes[i] <= 0xdf ? 2 : 3)
            {
                ++count;
            }

            return count;
        }

        public static int GetUtf8MaxBytesLength(int charsLength)
        {
            return charsLength * Utf8MaxBytesCount;
        }

        [MethodImpl(VersionDifferences.AggressiveInlining)]
        public static int GetUtf8MaxCharsLength(int bytesLength)
        {
            return bytesLength;
        }
    }