找回密码
 立即注册
首页 业界区 业界 字符编码知多少(一)

字符编码知多少(一)

欧阳雪枫 前天 06:20
前言

曾经在一场面试中,问到过UTF-8与UTF-16的区别,我一脸懵逼,惨遭羞辱。
最近在使用rider这个IDE的过程中,发现在visual studio中好好的代码,在rider中是乱码。
故此深入了解一下字符编码的前世今生。
前世:编码的蛮荒时代

由于计算机只能存储0/1二进制数据,因此计算机字符编码的起点,本质是为了解决字符与二进制数据的映射问题。
ASCII

计算机诞生于美国,因此最早的编码需求仅针对英文字符。1967年发布的ASCII(American Standard Code for Information Interchange)成为第一个通用编码:

  • 特点
    使用一个字节(8位)中的低7位来表示字符,因此可以表示2^7=128个字符。
    其内容主要包括,英语字母的大小写,数字0-9,标点符号,以及一些控制字符(换行\n,回车\r等符号)。
  • 优点
    简单,高效,仅占1个字节,完全满足英文场景下的计算机使用需求,成为计算机编码的底层基石。
  • 缺点
    只能表示英语字符,对于其它语言捉襟见肘。
眼见为实
  1.             var originalString = "C#编程";
  2.             Console.WriteLine($"originalString = {originalString}");
  3.             //ASCII编码
  4.             try
  5.             {
  6.                 var asciiBytes=Encoding.ASCII.GetBytes(originalString);
  7.                 Console.WriteLine($"ASCII 编码 (字节数: {asciiBytes.Length}):");
  8.                 Console.WriteLine($"  字节: {BitConverter.ToString(asciiBytes)}");
  9.                 Console.WriteLine($"  解码回字符串: "{Encoding.ASCII.GetString(asciiBytes)}"");
  10.             }
  11.             catch (Exception)
  12.             {
  13.                 Console.WriteLine("ASCII 解码失败");
  14.             }
复制代码
1.png

可以看到,对于英文字符C,与特殊符号# 。 ASCII能够正常编码解码,但对中文就无能为力了。
拓展ASCII与多字节字符(MBCS)

鉴于ASCII的缺点,各国为了能在计算机中表示自己的语言,开始制定自己的编码标准。
同样是英文语系的西欧语言,选择利用ASCII的最高位,将编码范围拓展到2^8=256个,比如ISO 8859-1。

  • 特点
    前128位与ASCII完全一致,拓展了后128位。补充了西欧重音字符以及特殊符号
  • 缺点
    仅覆盖西欧,对亚洲,非洲,俄文依旧不支持。
由于ASCII与ISO-8859-1无法满足多语言需求,各国纷纷开始定制专属的本地化编码,只为适配本国文字,毫无兼容性可言,因此开始了编码最混乱的时代。
像中文这样拥有成千上万字符的语言,靠拓展ASCII是远远不够的,一个字节最多表示256个字符,所以人们发明了多个字节来表示一个字符的方法。

  • GB2312
    作为中国的国家标准,GB2312大约收录了6763个常用汉字,后续推出的GBK作为其超集(支持1-2字节编码,覆盖更多生僻汉字和符号),GB18030则进一步扩展为1-4字节编码,兼容GBK并覆盖全球所有字符。
  • BIG5
    中国台湾/香港地区使用的繁体中文编码标准
  • Shift_JIS
    日本使用的编码标准
这个时候的核心问题在于,大多数国家和地区都各自为政,互不兼容。比如一个GBK编码的文件,在使用Big5编码的程序中打开,就会显示为乱码。
编码之间的混乱,导致了软件开发,数据交互的国际化几乎是不可能的。
眼见为实
  1.             // 错误地使用UTF-8来解码GBK的数据
  2.             try
  3.             {
  4.                 Encoding gbk = Encoding.GetEncoding("GBK");
  5.                 var gbkData = gbk.GetBytes(originalString);
  6.                 string garbledText = Encoding.UTF8.GetString(gbkData);
  7.                 Console.WriteLine($"GBK原始字节: {BitConverter.ToString(gbkData)}");
  8.                 Console.WriteLine($"用UTF-8错误解码的结果: "{garbledText}"");
  9.             }
  10.             catch (Exception)
  11.             {
  12.                 Console.WriteLine("解码失败");
  13.             }
复制代码
2.png

使用UTF-8来解码GBK,会乱码。
过渡:Unicode 字符集的诞生

本地化编码的乱象,催生出一个核心诉求:给全球所有的字符分配唯一的数字编号,彻底摆脱编码冲突。
所以Unicode诞生的应运而生,它作为字符=>数据的映射表,它为世界上所有的文字分配了一个唯一的数据编号,这个编号被称为码点,格式为U+XXXX(XXXX为16进制数字)。

  • 码点范围
    U+0000 ~ U+10FFFF,共划分17个平面。
  • 核心平面
    码点范围 U+0000 ~ U+FFFF,覆盖了全球 99% 的常用字符(中英文、数字、主流符号、日韩常用字等)。
  • 拓展平面
    剩余 16 个平面,用于表示生僻字、古文字、Emoji、专业符号等。
平面编号平面名称码点范围核心用途日常使用频率0号基本多文种平面(BMP)U+0000~U+FFFF英文/中文/数字/主流符号✅✅✅ 100%1号多文种补充平面(SMP)U+10000~U+1FFFFEmoji/古文字/音乐符号✅✅ 偶尔2~3号CJK扩展平面A/BU+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完全自定义字符/保留区❌ 无今生:UTF系列编码

虽然Unicode解决了编码的乱象,却没有规定这个数字编码应该怎么存储.
比如汉字”中“的码点是U+4E2D,这个十六进制的数字可以存为2字节,4字节。传输过程中还会遇到大端法/小端法的顺序问题。
而UTF(Unicode Transformation Format)就是这么一组编码方案。简单来说Unicode 是字符集(定义码点),而 UTF-8/16/32 是「Unicode 编码方案」(将码点转换为字节序列的规则)。
UTF-32

UTF-32是Unicode最早的实现方案,思路极其简单。固定使用4个固定字节来表示一个Unicode码点,无脑将码点转换成一个32位的二进制来存储。
优点


  • 编码/解码逻辑最简单:固定4字节表示1个Unicode码点,直接映射码点数值,无需判断长度/代理对/字节序;
  • 完美随机访问:字符位置与字节位置一一对应(字符数=字节数/4),极致适合字符精准定位的底层场景。
缺点


  • 空间浪费极致严重:所有字符均占4字节,纯英文文本比ASCII浪费300%,纯中文比UTF-8浪费33%,比UTF-16浪费100%;
  • 生态支持极少:几乎无主流协议/数据库/编程语言支持,仅部分底层系统/学术场景偶尔使用;
  • 传输/存储成本过高:完全不适合网络传输、文件存储,仅能用于内存临时处理的极特殊场景。
眼见为实
  1.                         //UTF32编码
  2.             try
  3.             {
  4.                 var utf32Bytes = Encoding.UTF32.GetBytes(originalString);
  5.                 Console.WriteLine($"UTF-32 (Unicode) 编码 (字节数: {utf32Bytes.Length}):");
  6.                 Console.WriteLine($"  字节: {BitConverter.ToString(utf32Bytes)}");
  7.                 Console.WriteLine($"  解码回字符串: "{Encoding.UTF32.GetString(utf32Bytes)}"");
  8.                 Console.WriteLine();
  9.             }
  10.             catch (Exception)
  11.             {
  12.                 Console.WriteLine("UTF-32 解码失败");
  13.             }
复制代码
3.png

可以看到,空间浪费极其严重。总共用了 16 个字节。每个字符都占 4 个字节。
UTF-16

于1996年推出,是对UTF-32的优化,也是可变长度编码的首次尝试。
针对Unicode的BMP字符,用2字节存储,超出BMP的拓展字符,用2个2字节(代理对)拼接存储(共4字节)
优点


  • BMP平面字符固定2字节:常用中文(U+4E00~U+9FFF)、英文、主流符号均占2字节,纯中文场景空间效率远高于UTF-8
  • 字符处理更高效:BMP内可随机访问字符,计算长度、截取字符串无需遍历,直接按字节数/2即可,适合编程语言内部存储;
  • 原生适配主流语言运行时:.NET的string类型、Java的char类型、JavaScript的String类型、Windows内核均原生基于UTF-16实现,无编码转换损耗。
缺点


  • 不兼容ASCII:英文/数字也占2字节,纯英文文本空间浪费100%,混合文本效率低于UTF-8;
  • 存在字节序问题:分UTF-16BE(大端)、UTF-16LE(小端),跨平台传输必须加BOM标记,否则必乱码;
  • 非BMP字符需代理对:生僻字/Emoji(扩展平面)需用「2个2字节」拼接(代理对),占4字节,解码时需额外处理代理对逻辑,易出bug;
  • 生态兼容差:互联网协议/数据库/前端均不默认支持,仅适用于编程语言内部/Windows系统,跨端传输需转UTF-8。
眼见为实
  1.                         //UTF16编码
  2.             try
  3.             {
  4.                 var utf16Bytes = Encoding.Unicode.GetBytes(originalString);
  5.                 Console.WriteLine($"UTF-16 (Unicode) 编码 (字节数: {utf16Bytes.Length}):");
  6.                 Console.WriteLine($"  字节: {BitConverter.ToString(utf16Bytes)}");
  7.                 Console.WriteLine($"  解码回字符串: "{Encoding.Unicode.GetString(utf16Bytes)}"");
  8.                 Console.WriteLine();
  9.             }
  10.             catch (Exception)
  11.             {
  12.                 Console.WriteLine("UTF-16 解码失败");
  13.             }
复制代码
4.png

相对UTF-32,存储空间占用有了极大改善,总共用了 8 个字节。每个字符都占 2 个字节(因为 "编程" 在 BMP 范围内)。
UTF-8

当前版本答案,于1992年推出。是目前最主流,最通用的Unicode编码方案。
根据字符的码点大小,动态分配字节数。
✔ 英文字母,数字占用1字节
✔ 欧洲,西亚文字占用2字节
✔ 中/日/韩文占用3字节
✔ 生僻字,Emoji,拓展符号占用4字节
优点


  • 完全兼容ASCII:ASCII字符(英文/数字/基础标点)占1字节,与传统ASCII文件无缝互通,无历史兼容成本;
  • 无字节序问题:无需BOM标记,跨平台(Linux/Mac/Windows)、跨系统传输零乱码风险
  • 空间效率均衡:混合文本(英文+中文+符号)场景下,整体空间利用率最高(英文1字节抵消中文3字节的损耗);
  • 生态兼容无敌:HTML/JSON/HTTP/数据库/主流编程语言/文件系统均默认支持,是互联网、后端开发的唯一标准
  • 错误容错性强:自同步编码规则,解码时可快速识别字符边界,局部错误不会导致整段乱码。
缺点


  • 中文/日韩字符占3字节:纯中文文本场景,比UTF-16多占用1字节/字符;
  • 变长解码稍复杂:1-4字节可变长度,需通过首字节判断字符长度,无法随机访问字符(计算字符串长度需遍历,而非直接取字节数/2);
  • 纯东亚文本空间稍浪费:无任何英文的纯中文/日文文本,空间占用比UTF-16高50%。
眼见为实
  1.             //UTF8编码
  2.             try
  3.             {
  4.                 var utf8Bytes = Encoding.UTF8.GetBytes(originalString);
  5.                 Console.WriteLine($"UTF-8 编码 (字节数: {utf8Bytes.Length}):");
  6.                 Console.WriteLine($"  字节: {BitConverter.ToString(utf8Bytes)}");
  7.                 Console.WriteLine($"  解码回字符串: "{Encoding.UTF8.GetString(utf8Bytes)}"");
  8.                 Console.WriteLine();
  9.             }
  10.             catch (Exception)
  11.             {
  12.                 Console.WriteLine("UTF-8 解码失败");
  13.             }
复制代码
5.png

总共用了 8 个字节。C(43), #(23) 各占 1 个字节,“编”(E7-BC-96) 和 “程”(E7-A8-8B) 各占 3 个字节。
挖坑待埋


  • 为什么中文在UTF-16下占用2字节,反而在UTF-8中占用3字节了?
  • UTF-16/UTF-8都是不定长编码规则,它们是如何解析的?
  • UTF-16的BOM标记长什么样?
  • 未完待续......

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册