CodeGym /课程 /C# SELF /主要的编码类型: UTF-8,

主要的编码类型: UTF-8, UTF-16, ASCII

C# SELF
第 37 级 , 课程 1
可用

1. 引言

我们已经知道,不管电脑多聪明,它们本身并不“懂”什么是字母 "A" 或符号 "⌘"。它们只懂 0 和 1,要把对人类有意义的字符翻译给机器,就需要一个翻译器——编码。

编码的历史就是妥协和演进的历史。起初一切很简单,后来变复杂,最后出现了比较通用的标准。我们按时间线过一遍。

历史起源

先从最来源说起。在上一讲我们提到过文本编码的老前辈 —— ASCII(读作 "ASCII")。简单回顾一下,它的全称是 American Standard Code for Information Interchange。名字里就说明了它为谁服务,为什么是“美国的”。

ASCII 在 1960 年代被制定,成为第一个被广泛使用的字符编码标准。它包含 128 个字符:

  • 拉丁字母(大写和小写): A-Z, a-z
  • 数字: 0-9
  • 标点符号: .,!?"'
  • 一些控制字符:换行、制表等

这 128 个字符每个都用一个字节编码,实际只用了 8 位中的 7 位(最高位通常空着或用于校验)。这对英文来说非常紧凑高效。


示例:
字符 'A' 在 ASCII 中编码为字节 0x41 (二进制 01000001)
字符 '!' 在 ASCII 中编码为字节 0x21 (二进制 00100001)

限制:
ASCII 最大的限制是:它只针对英语。如果你想写俄文("Hello")、德文("Grüße")或中文,ASCII 并不适用。它的表里根本没有这些字符。这催生了很多所谓的 代码页(Code Pages),用第八位把 128 个字符扩展到 256。例如,俄语常见的代码页有 CP1251(Windows Cyrillic)、KOI8-R 等。但问题是,这些代码页彼此不兼容:同一个字节在不同代码页里可能代表完全不同的字符!真正的巴别塔问题。

今天的实际使用:
纯 ASCII 格式如今很少作为通用文本文件使用,除非是非常特定的需求或遗留系统。不过它的遗产还在:许多现代编码都向后兼容 ASCII

我们来试试用 ASCII 写和读文件,然后再写入俄文看看会发生什么。

在 JetBrains Rider 中创建一个新的控制台项目,随便起名,比如 FileEncodingExplorer

using System;
using System.IO;
using System.Text;

string file = "ascii.txt";
string asciiText = "Hello, world!";
string cyrillicText = "你好,世界!";

// 写入 ASCII
using var writer = new StreamWriter(file, false, Encoding.ASCII);
writer.WriteLine(asciiText);
writer.WriteLine(cyrillicText);

// 从 ASCII 读取
using var reader = new StreamReader(file, Encoding.ASCII);
string content = reader.ReadToEnd();
Console.WriteLine("文件内容:");
Console.WriteLine(content);

Console.WriteLine("\n西里尔字母会被替换成 '?',因为 ASCII 不支持西里尔字符!");

运行这段代码你会看到,英文部分正常显示,而俄语(现在示例中是中文)会变成问号(?)或其他“未知”符号。这是因为 Encoding.ASCII 不知道如何把西里尔字符转换成字节,会替换为“安全”的替代字符(通常是 ?),或者某些字节按另一种编码解释后在 ASCII 下显示为别的字符。在我们的例子里,StreamWriter 在写入时会强制把不属于 ASCII 的字符替换为 ?。这说明了使用正确编码的重要性!

2. UTF-8:互联网和灵活性的王者

现在说一个非常重要、而且目前最流行的编码 —— UTF-8。它是大部分互联网、Linux 系统和现代应用在用的编码。

这是什么?
UTF-8 (Unicode Transformation Format - 8-bit) 是一种 Unicode 编码,解决了 UTF-16 在英文文本上效率不高的问题。UTF-8 是一种 可变长度编码,但设计得很聪明:

  • 属于普通 ASCII 的字符(代码从 0127)用 1 个字节 编码。最棒的是:这些字节与在 ASCII 中的表示完全一致!也就是说 UTF-8 向后兼容 ASCII
  • 其他字符用 24 个字节编码:
    • 西里尔字母通常用 2 个字节。
    • 很多带变音符的欧洲字符、阿拉伯文、希伯来文、希腊文也通常是 2 字节。
    • 中文/日文/韩文字符经常是 3 字节。
    • 罕见字符和部分表情符号是 4 字节。

UTF-8 的字节表示示例:

  • 字符 'A'(ASCII): 01000001(1 字节)
  • 字符 'я'(俄语): 11010001 10111111(2 字节)
  • 字符 '€'(欧元符号): 11100010 10000010 10101100(3 字节)
  • 字符 '😂'(表情): 11110000 10011111 10011000 10000010(4 字节)

为什么 UTF-8 是王者?

  1. 效率:对于包含大量 ASCII 字符的文本(典型的是英文、源码、配置文件),非常紧凑。
  2. 与 ASCII 的向后兼容:如果一个 UTF-8 文件只包含 ASCII 字符,可以当 ASCII 读取而不出问题!
  3. 通常没有 BOM:不同于 UTF-16UTF-8 一般不强制使用 BOM。虽然有时会看到它(比如 EF BB BF),但那是可选的“特性”,有时会带来麻烦(例如在解析某些格式或在 Linux 脚本中)。

缺点:

  • 可变长度会让某些操作变复杂(例如不扫描全文就跳到第 N 个字符),但在 C# 中通常没问题:string 对 Unicode 字符有自己的处理,不直接依赖文件编码。

实际应用:

  • 网页(HTML、CSS、JavaScript),
  • API(JSON、XML),
  • 配置文件,
  • 大多数编程语言的源代码,
  • Linux/Unix 系统。

我们来写一个并读取 UTF-8 的文件。

string file = "utf8.txt";
string text = "Hello, 世界! 😀 €";

// 写入 UTF-8(默认不带 BOM)
File.WriteAllText(file, text, Encoding.UTF8);

// 从 UTF-8 读取
string readText = File.ReadAllText(file, Encoding.UTF8);
Console.WriteLine(readText); // 全部正确读取!

运行后比较文件大小,你会看到针对混合文本的 utf8.txt 通常比 UTF-16 的小,如果文本全是英文,大小会接近 ASCII

3. UTF-16:面向全世界的 Unicode

代码页的巴别塔问题让开发者头疼,特别是软件走向全球后。需要一个通用的方案。于是出现了 UnicodeUnicode 不是某种编码,而是一个巨大表格,为每个已知字符分配一个独一无二的代码点(code point)。

这是什么?
UTF-16 (Unicode Transformation Format - 16-bit) 是一种最初设想用两个字节(16 位)编码所有 Unicode 字符的编码。

  • 大部分字符(BMP,直到 65535)用 2 字节编码。
  • 超出 BMP 的字符使用代理对(surrogate pairs)——共 4 字节。因此 UTF-16 也是可变长度,但通常被感知为每字符 2 字节。

字节序(Endianness)和 BOM:

  • Big-Endian (BE):高位字节在前。
  • Little-Endian (LE):低位字节在前。
  • 为了让读取程序知道顺序,文件开头常放一个 BOM(Byte Order Mark):
    • UTF-16 LE 的 BOM: FF FE
    • UTF-16 BE 的 BOM: FE FF
    在 C# 和 Windows 中默认使用的是 UTF-16 LE

优点:

  • 支持几乎世界上所有字符。
  • 在 BMP 范围内处理字符时比较简单(固定为 2 字节)。

缺点:

  • 对英文不太高效:每个 ASCII 字符占用 2 字节。
  • 存在 BOM 可能引发问题,如果读取端不期望它。

实际使用:
UTF-16 在 Windows 内部以及例如 Java 的字符串内部表示中使用广泛。Windows 的记事本保存包含西里尔文的文本时通常使用 UTF-16 LE 并带有 BOM

string file = "utf16.txt";
string text = "Hello, 世界! 👋";

// 写入 UTF-16(默认 Little-Endian,带 BOM)
File.WriteAllText(file, text, Encoding.Unicode);

// 从 UTF-16 读取
string readText = File.ReadAllText(file, Encoding.Unicode);
Console.WriteLine(readText); // 全部正确读取!

Console.WriteLine($"文件大小: {new FileInfo(file).Length} 字节");

运行后你会看到字符正确显示。对英文文本,UTF-16 的文件大约是 ASCIIUTF-8 的两倍(在 ASCII 范围内)。

4. 编码对比汇总表

把关键特性整理到一张表里,便于记忆。

编码 每个字符最少字节数 每个字符最多字节数 与 ASCII 的兼容性(直接) 是否默认使用 BOM(在 .NET 中) 应用示例
ASCII 1 1 完全兼容 旧系统、非常简单的文本数据、内部协议
UTF-16 2 4 是 (Encoding.Unicode) Windows 内部字符串表示、Java 字符串;Windows 文本文件
UTF-8 1 4 完全兼容 否 (Encoding.UTF8 在 .NET 5+); 是 (Encoding.UTF8 在 .NET Framework) Web(HTML、JSON)、配置文件、源代码、Linux/Unix

关于 Encoding.UTF8 在 .NET 的小提示:
历史上在 .NET Framework 中 Encoding.UTF8 默认会添加 BOM。在现代的 .NET(Core/5+)中这个行为变了:默认不再添加 BOM。如果你确实需要 BOM,可以使用 new UTF8Encoding(true)

5. 如何在 C# 中指定编码

正如示例中所见,要告诉 StreamReaderStreamWriter 用哪个“字典”,我们传入来自 System.Text.Encoding 的对象。

System.Text.Encoding 提供了一些常用选项:

  • Encoding.ASCII:用于处理 ASCII。
  • Encoding.UnicodeUTF-16 LE(带 BOM)。
  • Encoding.UTF8:UTF-8(在现代 .NET 中默认不带 BOM)。

其它编码可以通过 Encoding.GetEncoding 获取(比如 "windows-1251", "koi8-r"),但现在我们关注的是 Unicode。

// 写入 UTF-8
using var writer = new StreamWriter("my_file.txt", false, Encoding.UTF8);
writer.WriteLine("某些文本。");

// 从 UTF-16 读取
using var reader = new StreamReader("another_file.txt", Encoding.Unicode);
string content = reader.ReadToEnd();
Console.WriteLine(content);

就这么简单!StreamReaderStreamWriter 会根据你选择的编码负责把字符和字节互相转换。

6. 编码问题:乱码("mojibake")

我们前面已经看到把俄文写进 ASCII 会出现“乱码”。但更常见的问题是:文件以一种编码写入,但用另一种编码去读,这才是真正的有趣(也是痛苦)地方!

想象一下:信是用俄语写的并保存为 UTF-8,而你的客户端却把它当作 CP1251 来读。字节序列会被错误解释,你本该看到的 "Hello, мир!" 会变成乱码(英文叫 mojibake)。

原因只有一个:写入和读取时编码不一致。始终在读写两端使用相同编码,除非你明确在做转码。

string file = "mismatch.txt";
string russianText = "你好,世界!";

// 正确写入:UTF-8
File.WriteAllText(file, russianText, Encoding.UTF8);

// 错误读取:把 UTF-8 文件当成 ASCII 去读
string readAsAscii = File.ReadAllText(file, Encoding.ASCII);

Console.WriteLine($"原文: {russianText}");
Console.WriteLine($"作为 ASCII 读取: {readAsAscii}"); // 这里就会出现“乱码”!

运行程序,你会看到西里尔字母(在示例中是中文)变成问号或毫无意义的符号。下一课我们会讲更灵活的编码处理方式,如何避免这些问题,以及如何判断你要读取的文件实际使用了哪种编码。

2
任务
C# SELF, 第 37 级, 课程 1
已锁定
不同编码下文件大小比较
不同编码下文件大小比较
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION