用 FileStream
配合 Read
最直接高效
不需要把整个文件读进内存,
FileStream支持随机定位和局部读取。关键是打开时用
FileMode.Open和
FileAccess.Read,再调用
Read传入指定长度的字节数组即可。
常见错误是误用
File.ReadAllBytes或
StreamReader——前者强制加载全部内容,后者默认按字符解码,可能因 BOM 或编码问题提前截断或报错。 确保目标文件存在且有读取权限,否则抛
FileNotFoundException或
UnauthorizedAccessException字节数不要超过
int.MaxValue(2GB),但读前 N 字节一般远小于此,无需分块 如果 N 超过文件实际长度,
Read返回实际读到的字节数,需检查返回值,不能假设一定读满
var buffer = new byte[1024]; using var fs = new FileStream(@"C:\data.bin", FileMode.Open, FileAccess.Read, FileShare.Read); int bytesRead = fs.Read(buffer, 0, buffer.Length); // 实际读取长度
用 Span<byte></byte>
+ ReadAsync
更现代、零分配(.NET 5+)
如果在高吞吐场景(如服务端解析上传头),避免每次新建数组,可用栈分配的
Span<byte></byte>配合异步读取。注意:必须保证
Span生命周期不超过
ReadAsync调用本身,不可跨 await 边界持有。
这个方式不触发 GC 分配,适合循环处理大量小文件头部,但对单次简单读取意义不大,别为“新”而强行用。
ReadAsync在小数据量下未必比同步快,I/O 瓶颈不在 CPU,要测真实延迟再决定是否异步 若用
MemoryPool<byte></byte>复用缓冲区,需自行管理租借/归还,复杂度上升,仅当 profiling 显示分配成为瓶颈时考虑 别在
using块外 await,否则
FileStream可能提前释放
var buffer = stackalloc byte[512]; using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.SequentialScan); int n = await fs.ReadAsync(buffer); // .NET 5+
读取后怎么判断文件类型?别只看扩展名
扩展名可伪造,真正可靠的是魔数(magic number)——文件开头固定位置的字节序列。比如 PNG 必以
89 50 4E 47开头,ZIP 是
50 4B 03 04,UTF-8 BOM 是
EF BB BF。
注意:不同格式魔数长度不同,有的要读 2 字节(JPEG:
FF D8),有的要 4 字节甚至更多(ELF:
7F 45 4C 46)。别硬写死 4 字节就判断所有类型。 先确定你要支持的格式列表,查清各自魔数长度和偏移位置(有些魔数不在第 0 字节,如 PDF 的
SequenceEqual比较字节数组,比逐字节 if 判断更安全、可读 如果后续还要读更多内容(比如解析 PNG IHDR chunk),建议把已读的 buffer 和
FileStream.Position一起保留,避免重复 seek
大文件下 FileOptions.SequentialScan
能省点系统开销
告诉 Windows 文件系统:“我只会从前向后读一点,不会随机跳转”。内核会调整预读策略,减少不必要的磁盘寻道和缓存污染。虽然对只读前几百字节影响微乎其微,但加上没坏处,尤其批量处理时。
别跟
FileOptions.RandomAccess混用——后者是为反复 seek 设计的,和你“只读开头”的意图冲突。 该 flag 仅在 Windows 上生效,Linux/macOS 下被忽略,不影响功能 和
Buffering无关:即使关掉 OS 缓存(
FileOptions.NoBuffering),也要求对齐,反而让小读取变慢,完全没必要 如果文件路径来自用户输入,记得先用
Path.GetFullPath和
File.Exists校验,避免
DirectoryTraversal或空指针 实际最常踩的坑是:以为
StreamReader构造时传
detectEncodingFromByteOrderMarks: true就能安全读头,结果它内部会尝试读完整 BOM + 至少一个字符,可能触发超长读取或解码失败。真要分析二进制头,就老实用
FileStream。
