C#字节流与字符流
基本概念
| 维度 |
字节流 (Byte Stream) |
字符流 (Character Stream) |
| 基本单位 |
字节 (byte, 8-bit) |
字符 (char, UTF-16, 16-bit) |
| 核心基类 |
System.IO.Stream |
System.IO.TextReader / TextWriter |
| 典型实现 |
FileStream, MemoryStream, NetworkStream |
StreamReader, StreamWriter, StringReader |
| 数据本质 |
原始二进制数据 |
经过编码/解码后的文本数据 |
| 编码感知 |
❌ 无编码概念,读写原始字节 |
✅ 强依赖编码 (UTF-8, GBK, ASCII 等) |
| 换行处理 |
不识别换行符,仅作为字节 0x0A/0x0D |
自动识别 \n, \r\n,提供 ReadLine() |
| 适用场景 |
图片、视频、音频、加密数据、序列化对象、协议传输 |
日志、配置文件、CSV/JSON/XML 文本、源代码 |
| 性能特征 |
直接操作内存/磁盘,开销最小 |
需额外进行编解码转换,有 CPU 和缓冲开销 |
| 随机访问 |
✅ 支持 Seek, Position (取决于具体流) |
❌ 通常不支持(因变长编码导致字符位置≠字节位置) |
| BOM 处理 |
手动处理 |
自动检测/写入 BOM (Byte Order Mark) |
常用API
| 操作 |
字节流 (Stream) |
字符流 (TextReader/TextWriter) |
备注 |
| 读取 |
Read(byte[], offset, count) ReadByte() |
Read(char[], index, count) ReadLine() ReadToEnd() |
字符流提供高级文本读取方法 |
| 写入 |
Write(byte[], offset, count) WriteByte(byte) |
Write(string) WriteLine(string) Write(char) |
字符流自动将字符串编码为字节 |
| 刷新缓冲 |
Flush() |
Flush() |
字符流的 Flush 会同时刷新编码器状态 |
| 异步操作 |
ReadAsync, WriteAsync |
ReadLineAsync, WriteAsync |
两者均完整支持 async/await |
| 关闭/释放 |
Dispose() / using |
Dispose() / using |
字符流 Dispose 时默认会关闭底层字节流 |
| 定位 |
Seek(offset, origin) |
❌ 不支持 |
字符流是顺序抽象 |
性能对比
| 场景 |
推荐方案 |
原因 |
| 复制大文件(非文本,如视频文件) |
字节流 + 缓冲区 |
避免不必要的编解码开销 |
| 逐行解析日志文件 |
StreamReader.ReadLine() |
自动处理换行符和编码 |
| 网络协议收发 |
字节流 (NetworkStream) |
协议基于字节定义,编码由应用层控制 |
| 读写 JSON/XML 配置 |
字符流或专用序列化器 |
文本格式天然适合字符流 |
| 高频小文本写入 |
StreamWriter + 缓冲 |
内部有缓冲区,减少系统调用 |
| 需要精确控制字节偏移 |
字节流 |
字符流无法保证字符与字节的线性映射 |
| 跨平台文本文件 |
StreamWriter(utf8NoBom) |
.NET Core 的 UTF8Encoding(false) 避免 BOM 兼容问题 |
常见问题
| 陷阱 |
说明 |
解决方案 |
| 编码不匹配 |
用 GBK 写入,用 UTF-8 读取 → 乱码 |
始终显式指定编码,不要依赖默认值 |
| BOM 干扰 |
UTF-8 with BOM 文件被某些工具误解析 |
使用 new UTF8Encoding(false) 创建无 BOM 写入器 |
| 字符流 Seek |
试图对 StreamReader 做 BaseStream.Seek |
❌ 危险!会破坏内部解码器状态。应重新创建 Reader |
| 混合读写 |
同一文件交替使用字节流和字符流 |
❌ 缓冲区不同步。应统一使用一种流 |
| 未 Flush 丢数据 |
程序异常退出,字符流缓冲未写入 |
始终使用 using 或显式 Flush() |
| 大文件 ReadToEnd |
将整个文件加载到内存 → OOM |
使用 ReadLine() 或分块读取 |
| 网络流编码假设 |
假设 TCP 数据边界=字符边界 |
❌ TCP 是字节流协议,需自行处理粘包/拆包和编码 |
常用类
1、字节流
Stream (抽象基类)
├── FileStream // 文件读写(支持随机访问、共享模式)
├── MemoryStream // 内存 byte[] 后备存储
├── UnmanagedMemoryStream // 非托管内存指针访问(零拷贝)
├── NetworkStream // TCP Socket 封装
├── PipeStream (抽象) // 进程间通信管道
│ ├── NamedPipeServerStream
│ ├── NamedPipeClientStream
│ └── AnonymousPipeServerStream / ClientStream
├── BufferedStream // 装饰器:为无缓冲流添加字节级缓冲
├── GZipStream // 装饰器:GZip 压缩/解压
├── DeflateStream // 装饰器:Deflate 压缩/解压
├── SslStream // 装饰器:TLS/SSL 加密层
└── CryptoStream // 装饰器:对称加密/解密
└── UnmanagedMemoryStream // 非托管内存访问
2、字符流
TextReader (抽象基类 - 读取)
│ ├── StreamReader // 从字节流解码读取文本(带缓冲+BOM检测)
│ └── StringReader // 从 string 读取文本(纯内存)
TextWriter (抽象基类 - 写入)
├── StreamWriter // 编码写入字节流(带缓冲+AutoFlush)
├── StringWriter // 写入 StringBuilder(纯内存)
└── Console.Out / Error // 控制台输出(TextWriter 子类)
└── HttpWriter // ASP.NET 响应输出(不常用)
└── IndentedTextWriter // 带缩进控制的文本写入(用于代码生成等场景)
Encoding (抽象基类)
├── UTF8Encoding // UTF-8(可配置 BOM)
├── UnicodeEncoding // UTF-16 LE/BE
├── UTF32Encoding // UTF-32
├── ASCIIEncoding // ASCII (7-bit)
└── Decoder / Encoder // 有状态编解码器(处理跨缓冲区字符边界)
文件操作
1、读文件
1.1、字节流
static void Main(string[] args)
{
// 方式1
using (FileStream fs = new FileStream("d:/log/123456.txt", FileMode.Open))
{
byte[] buffer = new byte[fs.Length];
fs.Read(buffer, 0, buffer.Length);
// 指定编码将字节转换为字符串
string content = System.Text.Encoding.UTF8.GetString(buffer);
Console.WriteLine(content);
}
// 方式2
byte[] data = File.ReadAllBytes("d:/log/123456.txt");
Console.WriteLine(System.Text.Encoding.UTF8.GetString(data));
}
1.2、字符流
static void Main(string[] args)
{
// 方式1, 尝试检测编码
using (StreamReader sr = new StreamReader("d:/log/123456.txt"))
{
string content = sr.ReadToEnd();
Console.WriteLine(content);
}
// 方式2,自定义编码
using (StreamReader sr = new StreamReader("d:/log/123456.txt", System.Text.Encoding.UTF8))
{
string content = sr.ReadToEnd();
Console.WriteLine(content);
}
// 方式3
string data = File.ReadAllText("d:/log/123456.txt");
Console.WriteLine(data);
}
2、写文件
2.1、字节流
static void Main(string[] args)
{
const string fileName = "d:/log/123456.txt";
// 方式1
byte[] data = System.Text.Encoding.UTF8.GetBytes("静态便捷方法,内部自动创建/销毁字符流.");
File.WriteAllBytes(fileName, data);
// 方式2
using (FileStream fs = new FileStream(fileName, FileMode.Create | FileMode.Append))
{
string text = "FileStream 直接操作文件句柄,读写原始字节;字符流类内部封装了 FileStream + Encoding,提供文本语义.";
byte[] buffer = System.Text.Encoding.UTF8.GetBytes(text);
fs.Write(buffer, 0, buffer.Length);
}
Console.WriteLine(File.ReadAllText(fileName));
}
2.2、字符流
static void Main(string[] args)
{
const string fileName = "d:/log/123456.txt";
// 方式1
File.WriteAllText(fileName, "始终使用 using 语句块,它会在代码块结束时自动调用 Dispose(),从而关闭流并刷新缓冲区。");
File.WriteAllLines(fileName, ["根据需求不同,StreamWriter 提供了多种重载构造函数"]);
File.AppendAllText(fileName, "hello");
File.AppendAllLines(fileName, ["hello", "world"]);
var options = new FileStreamOptions
{
Mode = FileMode.Create | FileMode.Append,
Access = FileAccess.Write,
Share = FileShare.Read,
BufferSize = 8192,
Options = FileOptions.Asynchronous
};
// 方式2,自定义编码
using (StreamWriter sw = new StreamWriter(fileName, System.Text.Encoding.UTF8, options))
{
sw.WriteLine("StreamWriter 实现了 IDisposable 接口,因为它持有非托管资源(如文件句柄)。");
sw.WriteLine("必须在使用完毕后释放资源,否则会导致文件被锁定或内存泄漏。");
}
Console.WriteLine(File.ReadAllText(fileName));
}
内存操作
1、字节流
- 物理存储/传输层:全是 字节 (byte\[\])。
- 流层 (Stream):MemoryStream 操作字节数组。
- 编码层 (Encoding):Encoding.UTF8.GetBytes() 或 GetString() 负责转换。
- 应用层:字符串 (string) 或 字符数组 (char\[\])。
关键公式:
string → byte\[\] → MemoryStream (写入)
MemoryStream → byte\[\] → string (读取)
static void Main(string[] args)
{
// 1. 准备数据,模拟网络上下载的数据
string originalText = "1. 准备数据,模拟网络上下载的数据";
byte[] data = Encoding.UTF8.GetBytes(originalText);
// 2. 创建内存字节流
using (MemoryStream ms = new MemoryStream(data))
{
Console.WriteLine($"流长度: {ms.Length} 字节");
Console.WriteLine($"当前位置: {ms.Position}");
// 3. 读取字节 (模拟解析协议头或二进制结构)
byte firstByte = (byte)ms.ReadByte();
Console.WriteLine($"第一个字节: {firstByte} (对应字符: {(char)firstByte})");
// 4. 获取剩余所有字节
byte[] remainingBytes = new byte[ms.Length - ms.Position];
ms.Read(remainingBytes, 0, remainingBytes.Length);
// 5. 如果需要转回字符串,必须手动指定编码
string result = Encoding.UTF8.GetString(remainingBytes);
Console.WriteLine($"剩余内容: {result}");
}
}
2、字符流
static void Main(string[] args)
{
// 1. 创建底层内存字节流
using (MemoryStream ms = new MemoryStream())
{
// 2. 创建字符写入流 (StreamWriter),包装 MemoryStream
// leaveOpen: true 表示 StreamWriter 关闭时不要关闭底层的 MemoryStream
using (StreamWriter writer = new StreamWriter(ms, Encoding.UTF8, bufferSize: 1024, leaveOpen: true))
{
writer.WriteLine("创建字符写入流 (StreamWriter),包装 MemoryStream");
writer.WriteLine("leaveOpen: true 表示 StreamWriter 关闭时不要关闭底层的 MemoryStream");
writer.WriteLine("Flush确保数据从字符缓冲区刷入 MemoryStream");
writer.WriteLine("SeekFlush重要:重置流的位置到开头,否则读取时会从末尾开始");
// 确保数据从字符缓冲区刷入 MemoryStream
writer.Flush();
}
// 3. 重要:重置流的位置到开头,否则读取时会从末尾开始
ms.Seek(0, SeekOrigin.Begin);
// 4. 创建字符读取流 (StreamReader),包装 MemoryStream
using (StreamReader reader = new StreamReader(ms, Encoding.UTF8))
{
string? line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine($"读取到: {line}");
}
}
}
}