






















曾经在一场面试中,问到过UTF-8与UTF-16的区别,我一脸懵逼,惨遭羞辱。
最近在使用rider这个IDE的过程中,发现在visual studio中好好的代码,在rider中是乱码。
故此深入了解一下字符编码的前世今生。
由于计算机只能存储0/1二进制数据,因此计算机字符编码的起点,本质是为了解决字符与二进制数据的映射问题。
计算机诞生于美国,因此最早的编码需求仅针对英文字符。1967年发布的ASCII(American Standard Code for Information Interchange)成为第一个通用编码:
var originalString = "C#编程";
Console.WriteLine($"originalString = {originalString}");
//ASCII编码
try
{
var asciiBytes=Encoding.ASCII.GetBytes(originalString);
Console.WriteLine($"ASCII 编码 (字节数: {asciiBytes.Length}):");
Console.WriteLine($" 字节: {BitConverter.ToString(asciiBytes)}");
Console.WriteLine($" 解码回字符串: \"{Encoding.ASCII.GetString(asciiBytes)}\"");
}
catch (Exception)
{
Console.WriteLine("ASCII 解码失败");
}

可以看到,对于英文字符C,与特殊符号# 。 ASCII能够正常编码解码,但对中文就无能为力了。
鉴于ASCII的缺点,各国为了能在计算机中表示自己的语言,开始制定自己的编码标准。
同样是英文语系的西欧语言,选择利用ASCII的最高位,将编码范围拓展到2^8=256个,比如ISO 8859-1。
由于ASCII与ISO-8859-1无法满足多语言需求,各国纷纷开始定制专属的本地化编码,只为适配本国文字,毫无兼容性可言,因此开始了编码最混乱的时代。
像中文这样拥有成千上万字符的语言,靠拓展ASCII是远远不够的,一个字节最多表示256个字符,所以人们发明了多个字节来表示一个字符的方法。
这个时候的核心问题在于,大多数国家和地区都各自为政,互不兼容。比如一个GBK编码的文件,在使用Big5编码的程序中打开,就会显示为乱码。
编码之间的混乱,导致了软件开发,数据交互的国际化几乎是不可能的。
// 错误地使用UTF-8来解码GBK的数据
try
{
Encoding gbk = Encoding.GetEncoding("GBK");
var gbkData = gbk.GetBytes(originalString);
string garbledText = Encoding.UTF8.GetString(gbkData);
Console.WriteLine($"GBK原始字节: {BitConverter.ToString(gbkData)}");
Console.WriteLine($"用UTF-8错误解码的结果: \"{garbledText}\"");
}
catch (Exception)
{
Console.WriteLine("解码失败");
}

使用UTF-8来解码GBK,会乱码。
本地化编码的乱象,催生出一个核心诉求:给全球所有的字符分配唯一的数字编号,彻底摆脱编码冲突。
所以Unicode诞生的应运而生,它作为字符=>数据的映射表,它为世界上所有的文字分配了一个唯一的数据编号,这个编号被称为码点,格式为U+XXXX(XXXX为16进制数字)。
| 平面编号 | 平面名称 | 码点范围 | 核心用途 | 日常使用频率 |
|---|---|---|---|---|
| 0号 | 基本多文种平面(BMP) | U+0000~U+FFFF | 英文/中文/数字/主流符号 | ✅✅✅ 100% |
| 1号 | 多文种补充平面(SMP) | U+10000~U+1FFFF | Emoji/古文字/音乐符号 | ✅✅ 偶尔 |
| 2~3号 | CJK扩展平面A/B | U+20000~U+3FFFF | 中文生僻字/古汉字 | ✅ 极少 |
| 4~13号 | CJK扩展C~G+其他扩展 | U+40000~U+DFFFF | 极生冷僻字/专业符号 | ❌ 几乎无 |
| 14号 | 补充特殊用途平面(SSP) | U+E0000~U+EFFFF | 专业自定义字符 | ❌ 无 |
| 15~16号 | 私用使用区(PUA) | U+F0000~U+10FFFF | 完全自定义字符/保留区 | ❌ 无 |
虽然Unicode解决了编码的乱象,却没有规定这个数字编码应该怎么存储.
比如汉字”中“的码点是U+4E2D,这个十六进制的数字可以存为2字节,4字节。传输过程中还会遇到大端法/小端法的顺序问题。
而UTF(Unicode Transformation Format)就是这么一组编码方案。简单来说Unicode 是字符集(定义码点),而 UTF-8/16/32 是「Unicode 编码方案」(将码点转换为字节序列的规则)。
UTF-32是Unicode最早的实现方案,思路极其简单。固定使用4个固定字节来表示一个Unicode码点,无脑将码点转换成一个32位的二进制来存储。
//UTF32编码
try
{
var utf32Bytes = Encoding.UTF32.GetBytes(originalString);
Console.WriteLine($"UTF-32 (Unicode) 编码 (字节数: {utf32Bytes.Length}):");
Console.WriteLine($" 字节: {BitConverter.ToString(utf32Bytes)}");
Console.WriteLine($" 解码回字符串: \"{Encoding.UTF32.GetString(utf32Bytes)}\"");
Console.WriteLine();
}
catch (Exception)
{
Console.WriteLine("UTF-32 解码失败");
}

可以看到,空间浪费极其严重。总共用了 16 个字节。每个字符都占 4 个字节。
于1996年推出,是对UTF-32的优化,也是可变长度编码的首次尝试。
针对Unicode的BMP字符,用2字节存储,超出BMP的拓展字符,用2个2字节(代理对)拼接存储(共4字节)
string类型、Java的char类型、JavaScript的String类型、Windows内核均原生基于UTF-16实现,无编码转换损耗。 //UTF16编码
try
{
var utf16Bytes = Encoding.Unicode.GetBytes(originalString);
Console.WriteLine($"UTF-16 (Unicode) 编码 (字节数: {utf16Bytes.Length}):");
Console.WriteLine($" 字节: {BitConverter.ToString(utf16Bytes)}");
Console.WriteLine($" 解码回字符串: \"{Encoding.Unicode.GetString(utf16Bytes)}\"");
Console.WriteLine();
}
catch (Exception)
{
Console.WriteLine("UTF-16 解码失败");
}

相对UTF-32,存储空间占用有了极大改善,总共用了 8 个字节。每个字符都占 2 个字节(因为 "编程" 在 BMP 范围内)。
当前版本答案,于1992年推出。是目前最主流,最通用的Unicode编码方案。
根据字符的码点大小,动态分配字节数。
✔ 英文字母,数字占用1字节
✔ 欧洲,西亚文字占用2字节
✔ 中/日/韩文占用3字节
✔ 生僻字,Emoji,拓展符号占用4字节
//UTF8编码
try
{
var utf8Bytes = Encoding.UTF8.GetBytes(originalString);
Console.WriteLine($"UTF-8 编码 (字节数: {utf8Bytes.Length}):");
Console.WriteLine($" 字节: {BitConverter.ToString(utf8Bytes)}");
Console.WriteLine($" 解码回字符串: \"{Encoding.UTF8.GetString(utf8Bytes)}\"");
Console.WriteLine();
}
catch (Exception)
{
Console.WriteLine("UTF-8 解码失败");
}

总共用了 8 个字节。C(43), #(23) 各占 1 个字节,“编”(E7-BC-96) 和 “程”(E7-A8-8B) 各占 3 个字节。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。