C#字节流与字符流

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 试图对 StreamReaderBaseStream.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、字节流

c# 复制代码
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、字符流

c# 复制代码
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、字节流

c# 复制代码
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、字符流

c# 复制代码
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 (读取)

c# 复制代码
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、字符流

c# 复制代码
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}");
            }
        }
    }
}
相关推荐
大白话_NOI1 小时前
【洛谷 P1024 】[NOIP2001 提高组] 一元三次方程求解 - 详细分析与C++实现
c++·算法
Matthew_zhu_1 小时前
P3374 【模板】树状数组 1 题解
算法
随意起个昵称1 小时前
区间dp-进阶题目1(进阶合并)
c++·算法·动态规划
伶俜661 小时前
鸿蒙原生应用实战(四)ArkUI 语音变声器:录音 + 4 种音效 + 音调变换算法
算法·华为·harmonyos
AKA__Zas2 小时前
芝士算法(滑动窗口片 2.0)
java·算法·leetcode·学习方法
变量未定义~2 小时前
摆放小球 、dp求解组合数、求解组合数2
数据结构·算法
加油码2 小时前
位图 BitMap:用一个 bit 管一个状态,空间直接省到位
c++·算法
四代水门2 小时前
LeetCode刷算法题(C++)
c++·算法·leetcode
一头老黄牛@2 小时前
飞书 × OpenClaw 接入指南:不用服务器,用长连接把机器人跑起来
数据结构·人工智能·程序人生·算法·决策树·自动化·推荐算法