zoukankan      html  css  js  c++  java
  • 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;
            }
        }
  • 相关阅读:
    剑指Offer-30.连续子数组的最大和(C++/Java)
    剑指Offer-29.最小的K个数(C++/Java)
    UVA 1616 Caravan Robbers 商队抢劫者(二分)
    UVA 10570 Meeting with Aliens 外星人聚会
    UVA 11093 Just Finish it up 环形跑道 (贪心)
    UVA 12673 Erratic Expansion 奇怪的气球膨胀 (递推)
    UVA 10954 Add All 全部相加 (Huffman编码)
    UVA 714 Copying Books 抄书 (二分)
    UVALive 3523 Knights of the Round Table 圆桌骑士 (无向图点双连通分量)
    codeforecs Gym 100286B Blind Walk
  • 原文地址:https://www.cnblogs.com/Dogwei/p/11236706.html
Copyright © 2011-2022 走看看