在C#中处理二进制文件,核心思路是使用FileStream作为底层字节通道,再借助BinaryWriter和BinaryReader完成类型化数据的写入与读取。这种方式跳过了文本编码转换,直接以内存中的字节表现形式持久化数据,适合对性能和体积敏感的场景。

一、二进制流操作的基础对象
C#读写二进制文件主要依赖三个类。FileStream负责在磁盘文件和内存之间传输原始字节,它不关心数据含义,只保证字节正确到达。在其之上包裹BinaryWriter可以将int、float、string等类型按特定格式写成字节;BinaryReader则反向将字节还原为对应类型。三者配合形成完整的二进制数据流管道。
创建FileStream时通常需要指定文件路径、打开模式以及读写权限。例如使用FileMode.Create会新建或覆盖文件,FileAccess.Write限制为只写。由于这些类实现了IDisposable接口,必须用using语句包裹,确保即使发生异常也能释放文件句柄,避免文件被进程长期占用导致其他操作失败。
using System;
using System.IO;
class Demo
{
static void Main()
{
// 创建文件流,准备写入
using (FileStream fs = new FileStream("data.bin", FileMode.Create, FileAccess.Write))
using (BinaryWriter bw = new BinaryWriter(fs))
{
bw.Write(100); // 写入整数
bw.Write(3.14f); // 写入单精度浮点
bw.Write("hello"); // 写入字符串
}
Console.WriteLine("写入完成");
}
}
二、使用BinaryWriter写入常见数据类型
BinaryWriter提供了大量重载的Write方法,能够写入布尔值、字节、字符、各种长度的整数与浮点数,以及以长度前缀方式编码的字符串。字符串默认采用UTF-8编码并在前面附加一个七位压缩长度值,读取时BinaryReader会自动按同样规则解析,开发者无需手动计算字节数。
如果需要和某种固定格式协议对接,比如网络报文头部,可能要求字符串以固定字节数存储而非变长。此时应先将字符串用指定编码转为byte[],再调用Write(byte[])写入,读取时用ReadBytes取回定长数组后解码。下面示例展示定长写入用户名的方法,避免长度前缀带来的格式不兼容。
using System;
using System.IO;
using System.Text;
class FixedWriter
{
static void Main()
{
using (FileStream fs = new FileStream("user.bin", FileMode.Create))
using (BinaryWriter bw = new BinaryWriter(fs))
{
string name = "tom";
byte[] nameBytes = Encoding.ASCII.GetBytes(name);
byte[] buffer = new byte[16]; // 固定16字节用户名区
Array.Copy(nameBytes, buffer, nameBytes.Length);
bw.Write(buffer); // 写入定长数据
bw.Write(25); // 写入年龄
}
}
}
三、使用BinaryReader读取并还原数据
读取时必须严格按照写入时的顺序与类型调用BinaryReader的对应方法。例如先写int再写float,读取就要先ReadInt32再ReadSingle。顺序错乱会导致读取到错误字节,甚至抛出EndOfStreamException。对于自定义对象,推荐先约定好字段布局,然后逐一读写,保持两端结构同步。
当文件可能由其他语言生成时,还需注意字节序差异。C#的BinaryWriter在Windows上默认小端序,而某些协议要求大端序。此时不能用默认方法,应借助BitConverter翻转数组后再写。以下代码演示读取前面示例写的定长用户名与年龄,并正确处理编码。
using System;
using System.IO;
using System.Text;
class ReaderDemo
{
static void Main()
{
using (FileStream fs = new FileStream("user.bin", FileMode.Open, FileAccess.Read))
using (BinaryReader br = new BinaryReader(fs))
{
byte[] nameBuf = br.ReadBytes(16); // 读取16字节
string name = Encoding.ASCII.GetString(nameBuf).TrimEnd(' ');
int age = br.ReadInt32(); // 读取年龄
Console.WriteLine($"name={name}, age={age}");
}
}
}
四、自定义对象的序列化读写实践
真实项目中常需要保存整个对象,比如游戏角色状态。虽然可用BinaryFormatter,但该类已因安全问题被微软标记废弃。更稳妥的做法是手写读写逻辑:在类里加Save和Load方法,显式控制每个字段。这样既能跨平台又清晰可控,还方便版本演进时做兼容。
下例定义一个Player类,包含坐标与等级,分别用BinaryWriter和BinaryReader完成存取。若后续增加新字段,可在文件头写入版本号,读取时按版本分支处理,避免旧文件无法解析。这种手法在长周期项目中非常实用。
using System;
using System.IO;
class Player
{
public float X { get; set; }
public float Y { get; set; }
public int Level { get; set; }
public void Save(string path)
{
using (var fs = new FileStream(path, FileMode.Create))
using (var bw = new BinaryWriter(fs))
{
bw.Write(X);
bw.Write(Y);
bw.Write(Level);
}
}
public static Player Load(string path)
{
using (var fs = new FileStream(path, FileMode.Open))
using (var br = new BinaryReader(fs))
{
return new Player
{
X = br.ReadSingle(),
Y = br.ReadSingle(),
Level = br.ReadInt32()
};
}
}
}
五、常见错误与性能注意点
第一类常见错误是忘记释放流对象。未使用using且未手动调用Dispose,文件会一直被锁定,直到垃圾回收才释放,期间其他进程无法删除或修改。第二类是读写类型不匹配,比如写入long却用ReadInt32读取,直接破坏后续所有字段。建议在协议文档中明确定义每个偏移处的类型与长度。
性能方面,若频繁写入零散小数据,应增大FileStream缓冲区或先在内存用MemoryStream攒批再一次性落盘。另外BinaryWriter的字符串长度前缀采用变长编码,对极大量短字符串场景略有开销,可改用定长编码优化。掌握这些细节后,C#二进制数据流操作就能既安全又高效。
C#binary_fileFileStream修改时间:2026-08-11 01:27:32