最稳妥方式是用 FileStream 配合 CopyToAsync。需确保目标目录存在、源流 Position 为 0、显式指定 FileOptions.Asynchronous,避免提前释放源流,禁用 ToArray() 等全内存加载操作。

用 FileStream 配合 CopyTo 最稳妥
只要流支持读取(CanRead == true)且未被关闭,直接用内置的 CopyTo 是最安全、最省心的方式。它自动处理缓冲区、分块读写,还兼容异步(CopyToAsync)。
- 务必确保目标目录已存在,
FileStream不会自动创建父级路径,否则抛DirectoryNotFoundException - 推荐显式指定
FileOptions.Asynchronous(尤其大文件),避免阻塞线程池 - 若源流位置不在开头(
Position != 0),需先调Seek(0, SeekOrigin.Begin),否则可能写入空内容
using var fileStream = new FileStream("output.bin", FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.Asynchronous); await inputStream.CopyToAsync(fileStream);
手动读写时注意 Buffer 大小和 Read 返回值
自己用 Read + Write 循环虽然可控,但容易忽略返回值含义——它只代表本次实际读到的字节数,未必等于缓冲区长度。不检查就直接写满缓冲区,会导致末尾数据错乱或重复。
- 缓冲区大小建议设为 4096 或 8192,太小性能差,太大无意义(I/O 层有内部优化)
- 必须用
int bytesRead = stream.Read(buffer, 0, buffer.Length)判断真实读取量 - 写入时也得传入
bytesRead,而非buffer.Length
var buffer = new byte[8192]; int bytesRead; while ((bytesRead = inputStream.Read(buffer, 0, buffer.Length)) > 0) {outputStream.Write(buffer, 0, bytesRead); }
别在 using 里提前释放源 Stream
常见错误是把输入流也放进 using 块,结果 CopyTo 还没跑完流就被关了,抛出 ObjectDisposedException。谁打开谁负责关——如果调用方传入流,通常应由调用方决定生命周期。
- 函数签名中明确标注参数是否会被释放,例如:
void SaveStream(Stream input, string path, bool leaveOpen = false) - 若设
leaveOpen = true,则FileStream构造时要传FileOptions.None并避免在using中包裹输入流 - ASP.NET Core 中接收上传的
IFormFile.OpenReadStream()返回流默认不可重用,且不能留开,必须一次性读完
大文件或网络流要防内存暴涨
用 MemoryStream.ToArray() 或 ReadAllBytes() 加载整个流到内存,几 MB 就可能触发 GC 压力,上百 MB 直接 OOM。这不是“能不能”的问题,是“一定不能”。
-
CopyTo和手动循环都是流式处理,内存占用恒定(仅缓冲区大小) - 若后续还需读取该文件,优先写磁盘;若只是临时转换,考虑用
TempFileCollection或Path.GetTempFileName()管理生命周期 - HTTP 场景下,Nginx/IIS 可能限制单次请求体大小,默认 30MB,超限会直接 413,和 C# 代码无关
真正容易被忽略的是流的位置状态和所有权归属——同一个 Stream 实例可能被多个组件共享,Position 被意外移动,或关闭时机错配,这类问题在线上环境往往表现为偶发性写入为空或截断,排查成本远高于写对第一行代码。