文件 I/O 与流操作
大约 11 分钟约 3341 字
文件 I/O 与流操作
简介
文件 I/O 是应用程序与文件系统交互的基础能力。.NET 提供了 File、Directory、Stream、StreamReader/Writer、BinaryReader/Writer 等丰富的 API。掌握文件读写、目录操作、流处理和异步 I/O,是构建日志、文件管理、数据处理等功能的必备技能。
.NET 的 I/O 系统建立在 Stream 抽象之上,所有 I/O 操作(文件、网络、内存、管道)都可以通过 Stream 统一处理。理解 Stream 的工作原理、缓冲机制和异步 I/O 模型,是编写高效 I/O 代码的基础。
特点
I/O 方案选择
文件大小与方案对应
// ==========================================
// 文件大小 → 推荐方案
// ==========================================
// < 1 MB → File.ReadAllText / File.WriteAllText(最简单)
// 1-100 MB → StreamReader/Writer(流式处理,内存友好)
// 100 MB+ → FileStream + 分块读写(控制内存使用)
// GB 级 → MemoryMappedFile(内存映射,OS 级分页)
// 实时流 → Pipe / Named Pipe(进程间通信)
// ==========================================
// 文件 I/O 性能对比
// ==========================================
// ReadAllText: 一次性读入内存,小文件最快
// StreamReader: 流式读取,内存可控
// FileStream: 最灵活,可控制缓冲区大小
// MemoryMapped: 超大文件最快(OS 级分页缓存)
// Pipe: 实时数据流,无磁盘 I/O文件读写
File 静态类
/// <summary>
/// File 类 — 简单文件操作
/// </summary>
// 读写文本
await File.WriteAllTextAsync("output.txt", "Hello World");
string content = await File.ReadAllTextAsync("input.txt");
// 读写行
await File.WriteAllLinesAsync("lines.txt", new[] { "第一行", "第二行", "第三行" });
string[] lines = await File.ReadAllLinesAsync("lines.txt");
// 追加内容
await File.AppendAllTextAsync("log.txt", $"[{DateTime.Now}] 应用启动\n");
// 读写字节
byte[] data = await File.ReadAllBytesAsync("image.png");
await File.WriteAllBytesAsync("copy.png", data);
// 文件信息
bool exists = File.Exists("data.txt");
var info = new FileInfo("data.txt");
long size = info.Length;
DateTime modified = info.LastWriteTime;
// ==========================================
// File 静态方法的注意事项
// ==========================================
// ReadAllText/ReadAllBytes 一次性读入内存
// 如果文件很大(> 100 MB),可能导致 OutOfMemoryException
// 反面示例:
var hugeContent = await File.ReadAllTextAsync("1GB-file.log"); // 可能 OOM!
// 正确做法 — 使用流式读取
await using var stream = File.OpenRead("1GB-file.log");
await using var reader = new StreamReader(stream);
while (await reader.ReadLineAsync() is { } line)
{
ProcessLine(line); // 逐行处理,内存占用恒定
}流操作
/// <summary>
/// Stream — 灵活的流式读写
/// </summary>
// 使用 FileStream 读写
await using var fs = new FileStream("data.bin", FileMode.Create, FileAccess.Write);
await using var writer = new BinaryWriter(fs);
writer.Write(42);
writer.Write("Hello");
writer.Write(3.14);
// 读取
await using var fs2 = new FileStream("data.bin", FileMode.Open);
await using var reader = new BinaryReader(fs2);
int num = reader.ReadInt32(); // 42
string text = reader.ReadString(); // "Hello"
double pi = reader.ReadDouble(); // 3.14
// StreamReader/StreamWriter — 文本流
await using var sw = new StreamWriter("log.txt", append: true, Encoding.UTF8);
await sw.WriteLineAsync($"[{DateTime.Now:HH:mm:ss}] 日志消息");
await using var sr = new StreamReader("log.txt", Encoding.UTF8);
string? line;
while ((line = await sr.ReadLineAsync()) != null)
{
Console.WriteLine(line);
}
// 流复制
await using var source = File.OpenRead("large.mp4");
await using var dest = File.OpenWrite("copy.mp4");
await source.CopyToAsync(dest);
// ==========================================
// FileStream 的高级选项
// ==========================================
await using var optimizedStream = new FileStream(
"large.dat",
FileMode.OpenOrCreate,
FileAccess.ReadWrite,
FileShare.Read, // 允许其他进程读取
bufferSize: 65536, // 64KB 缓冲区(默认 4KB)
FileOptions.Asynchronous | // 异步 I/O
FileOptions.SequentialScan // 顺序读取优化
);
// ==========================================
// 流式搜索大文件
// ==========================================
public static async IAsyncEnumerable<string> SearchFileAsync(
string filePath, string keyword)
{
await using var stream = File.OpenRead(filePath);
await using var reader = new StreamReader(stream);
long lineNumber = 0;
while (await reader.ReadLineAsync() is { } line)
{
lineNumber++;
if (line.Contains(keyword, StringComparison.OrdinalIgnoreCase))
yield return $"行 {lineNumber}: {line}";
}
}
// 使用
await foreach (var match in SearchFileAsync("access.log", "ERROR"))
Console.WriteLine(match);目录操作
Directory 类
/// <summary>
/// Directory — 目录管理
/// </summary>
// 创建目录
Directory.CreateDirectory("output/logs/2024");
var dirInfo = new DirectoryInfo("output");
dirInfo.CreateSubdirectory("temp");
// 遍历目录
var files = Directory.GetFiles("output", "*.json", SearchOption.AllDirectories);
var dirs = Directory.GetDirectories("output");
// 使用 EnumerationOptions(.NET 5+)
var options = new EnumerationOptions
{
IgnoreInaccessible = true,
RecurseSubdirectories = true,
MatchCasing = MatchCasing.CaseInsensitive
};
var csFiles = Directory.GetFiles("src", "*.cs", options);
// 目录信息
var dir = new DirectoryInfo("output");
long totalSize = dir.EnumerateFiles("*", SearchOption.AllDirectories).Sum(f => f.Length);
// 移动/删除
Directory.Move("old_path", "new_path");
Directory.Delete("temp", recursive: true);目录遍历的高级用法
// ==========================================
// 递归遍历目录(排除特定目录)
// ==========================================
public static IEnumerable<string> EnumerateFilesSafe(
string rootPath,
string searchPattern = "*",
params string[] excludeDirectories)
{
var excluded = new HashSet<string>(excludeDirectories, StringComparer.OrdinalIgnoreCase);
var stack = new Stack<string>();
stack.Push(rootPath);
while (stack.Count > 0)
{
var currentDir = stack.Pop();
IEnumerable<string> files;
IEnumerable<string> subDirs;
try
{
files = Directory.EnumerateFiles(currentDir, searchPattern);
subDirs = Directory.EnumerateDirectories(currentDir);
}
catch (UnauthorizedAccessException)
{
continue; // 跳过无权限的目录
}
foreach (var file in files)
yield return file;
foreach (var subDir in subDirs)
{
if (!excluded.Contains(Path.GetFileName(subDir)))
stack.Push(subDir);
}
}
}
// 使用 — 排除 node_modules 和 .git
foreach (var file in EnumerateFilesSafe(".", "*.cs", "node_modules", ".git", "bin", "obj"))
Console.WriteLine(file);
// ==========================================
// 计算目录大小
// ==========================================
public static async Task<long> GetDirectorySizeAsync(string path)
{
long size = 0;
foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories))
{
try
{
var info = new FileInfo(file);
size += info.Length;
}
catch { }
}
return size;
}
// ==========================================
// 安全的目录删除
// ==========================================
public static void DeleteDirectoryIfExists(string path)
{
if (!Directory.Exists(path)) return;
try
{
Directory.Delete(path, recursive: true);
}
catch (IOException)
{
// 文件可能被锁定,等待后重试
foreach (var file in Directory.EnumerateFileSystemEntries(path, "*", SearchOption.AllDirectories))
{
try { File.SetAttributes(file, FileAttributes.Normal); } catch { }
}
Directory.Delete(path, recursive: true);
}
}路径处理
Path 类
/// <summary>
/// Path — 跨平台路径操作
/// </summary>
// 路径组合(自动处理分隔符)
string path = Path.Combine("data", "logs", "app.log"); // data/logs/app.log
// 获取路径部分
Path.GetDirectoryName("/data/logs/app.log"); // /data/logs
Path.GetFileName("/data/logs/app.log"); // app.log
Path.GetExtension("report.pdf"); // .pdf
Path.GetFileNameWithoutExtension("data.csv"); // data
// 临时文件
string tempFile = Path.GetTempFileName();
string tempDir = Path.Combine(Path.GetTempPath(), "myapp");
// 相对路径
string relative = Path.GetRelativePath("/data/logs", "/data/config/settings.json");
// ../config/settings.json
// 路径变更
string newPath = Path.ChangeExtension("data.csv", ".json"); // data.json
// ==========================================
// 跨平台路径注意事项
// ==========================================
// Windows: 反斜杠 \, 不区分大小写, 盘符 C:\
// Linux/macOS: 正斜杠 /, 区分大小写, 无盘符
// 使用 Path.Combine 代替字符串拼接
// 好:
var safePath = Path.Combine(baseDir, "subdir", "file.txt");
// 不好:
var unsafePath = baseDir + "\\" + "subdir" + "\\" + "file.txt"; // 跨平台问题
// .NET 6+ 支持路径拼接运算符
var combined = baseDir / "subdir" / "file.txt";文件监控
FileSystemWatcher
/// <summary>
/// FileSystemWatcher — 监控文件变更
/// </summary>
using var watcher = new FileSystemWatcher("C:/Logs")
{
Filter = "*.log",
NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.Size,
IncludeSubdirectories = true,
EnableRaisingEvents = true
};
watcher.Created += (sender, e) =>
{
Console.WriteLine($"文件创建:{e.FullPath}");
};
watcher.Changed += (sender, e) =>
{
Console.WriteLine($"文件修改:{e.FullPath}");
};
watcher.Renamed += (sender, e) =>
{
Console.WriteLine($"文件重命名:{e.OldFullPath} → {e.FullPath}");
};
watcher.Deleted += (sender, e) =>
{
Console.WriteLine($"文件删除:{e.FullPath}");
};
Console.WriteLine("监控中...按任意键退出");
Console.ReadKey();FileSystemWatcher 的已知问题
// ==========================================
// FileSystemWatcher 的限制和解决方案
// ==========================================
// 问题 1: 事件可能丢失
// 在高频率文件变更时,内部缓冲区可能溢出
// 解决: 增大缓冲区
watcher.InternalBufferSize = 64 * 1024; // 64KB(默认 8KB)
// 问题 2: 事件可能重复
// 一个保存操作可能触发多次 Changed 事件
// 解决: 使用防抖动(debounce)
public class DebouncedFileSystemWatcher : IDisposable
{
private readonly FileSystemWatcher _watcher;
private readonly Dictionary<string, Timer> _debounceTimers = new();
private readonly Action<string> _onChanged;
private readonly TimeSpan _debounceInterval;
public DebouncedFileSystemWatcher(
string path,
string filter,
Action<string> onChanged,
TimeSpan? debounceInterval = null)
{
_onChanged = onChanged;
_debounceInterval = debounceInterval ?? TimeSpan.FromSeconds(1);
_watcher = new FileSystemWatcher(path, filter)
{
EnableRaisingEvents = true,
IncludeSubdirectories = true,
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName
};
_watcher.Changed += OnChanged;
_watcher.Created += OnChanged;
_watcher.Deleted += OnChanged;
}
private void OnChanged(object sender, FileSystemEventArgs e)
{
// 防抖动:同一个文件在短时间内多次变更,只触发一次
if (_debounceTimers.TryGetValue(e.FullPath, out var existingTimer))
existingTimer.Dispose();
var timer = new Timer(_ =>
{
_debounceTimers.Remove(e.FullPath);
_onChanged(e.FullPath);
}, null, (int)_debounceInterval.TotalMilliseconds, Timeout.Infinite);
_debounceTimers[e.FullPath] = timer;
}
public void Dispose()
{
_watcher.Dispose();
foreach (var timer in _debounceTimers.Values)
timer.Dispose();
}
}内存映射文件
高性能大文件读写
/// <summary>
/// MemoryMappedFile — 高性能大文件访问
/// </summary>
// 创建内存映射文件
using var mmf = MemoryMappedFile.CreateFromFile("large_data.bin",
FileMode.OpenOrCreate, "LargeDataMap", 1024 * 1024 * 100); // 100MB
// 写入
using (var accessor = mmf.CreateViewAccessor())
{
accessor.Write(0, 12345);
accessor.WriteArray(4, new byte[] { 1, 2, 3 }, 0, 3);
}
// 读取
using (var accessor = mmf.CreateViewAccessor())
{
int value = accessor.ReadInt32(0);
}
// 流式访问
using var stream = mmf.CreateViewStream();
using var reader = new StreamReader(stream);
string content = await reader.ReadToEndAsync();实际应用 — 日志轮转
/// <summary>
/// 简单的日志轮转实现
/// </summary>
public class RotatingFileLogger : IDisposable
{
private readonly string _logDirectory;
private readonly string _baseFileName;
private readonly long _maxFileSizeBytes;
private readonly int _maxFiles;
private StreamWriter? _currentWriter;
private string _currentFilePath;
private long _currentFileSize;
public RotatingFileLogger(
string logDirectory,
string baseFileName = "app",
long maxFileSizeBytes = 10 * 1024 * 1024, // 10 MB
int maxFiles = 10)
{
_logDirectory = logDirectory;
_baseFileName = baseFileName;
_maxFileSizeBytes = maxFileSizeBytes;
_maxFiles = maxFiles;
Directory.CreateDirectory(_logDirectory);
RotateFile();
}
public async Task LogAsync(string message)
{
CheckRotation();
await _currentWriter!.WriteLineAsync(
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {message}");
await _currentWriter.FlushAsync();
_currentFileSize += Encoding.UTF8.GetByteCount(message) + Environment.NewLine.Length;
}
private void CheckRotation()
{
if (_currentFileSize >= _maxFileSizeBytes)
RotateFile();
}
private void RotateFile()
{
_currentWriter?.Dispose();
// 删除旧文件
var files = Directory.GetFiles(_logDirectory, $"{_baseFileName}*.log")
.OrderByDescending(f => f)
.Skip(_maxFiles - 1)
.ToArray();
foreach (var file in files)
{
try { File.Delete(file); } catch { }
}
// 创建新文件
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
_currentFilePath = Path.Combine(_logDirectory, $"{_baseFileName}_{timestamp}.log");
_currentWriter = new StreamWriter(_currentFilePath, append: false, Encoding.UTF8)
{
AutoFlush = true
};
_currentFileSize = 0;
}
public void Dispose()
{
_currentWriter?.Dispose();
}
}Pipe 管道
匿名管道和命名管道
// ==========================================
// Pipe — 线程间/进程间通信
// ==========================================
// 匿名管道(父子进程间通信)
// 父进程
using var pipeServer = new AnonymousPipeServerStream(
PipeDirection.Out, HandleInheritability.Inheritable);
var processInfo = new ProcessStartInfo
{
FileName = "child.exe",
Arguments = pipeServer.GetClientHandleAsString(),
UseShellExecute = false
};
// ==========================================
// 命名管道(任意进程间通信)
// ==========================================
// 服务端
var server = new NamedPipeServerStream("MyPipe",
PipeDirection.InOut,
maxNumberOfServerInstances: 1,
PipeTransmissionMode.Byte,
PipeOptions.Asynchronous);
await server.WaitForConnectionAsync();
using var reader = new StreamReader(server);
using var writer = new StreamWriter(server);
var message = await reader.ReadLineAsync();
await writer.WriteLineAsync($"已收到: {message}");
// 客户端
using var client = new NamedPipeClientStream(".", "MyPipe",
PipeDirection.InOut);
await client.ConnectAsync(5000);
using var clientWriter = new StreamWriter(client);
using var clientReader = new StreamReader(client);
await clientWriter.WriteLineAsync("Hello from client");
var response = await clientReader.ReadLineAsync();
// ==========================================
// System.IO.Pipelines (.NET Core 2.1+)
// ==========================================
// 高性能的管道抽象,用于零拷贝数据处理
var pipe = new Pipe();
var writer = pipe.Writer;
var reader = pipe.Reader;
// 写入
Span<byte> buffer = writer.GetSpan(1024);
var written = Encoding.UTF8.GetBytes("Hello", buffer);
writer.Advance(written);
await writer.FlushAsync();
// 读取
var result = await reader.ReadAsync();
var input = result.Buffer;
// 处理数据...
reader.AdvanceTo(input.End);优点
缺点
总结
文件 I/O 核心选择:小文件用 File 静态方法,大文件用 Stream 流式处理,超大文件用 MemoryMappedFile。异步操作一律用 Async 版本。路径处理用 Path 类确保跨平台兼容。文件监控用 FileSystemWatcher,但注意事件可能丢失。
核心原则:
- 始终用异步 — ReadAsync/WriteAsync 避免阻塞线程池
- 用 using 管理资源 — Stream、StreamReader、Writer 都要 Dispose
- 处理异常 — IO 异常很常见, UnauthorizedAccessException、IOException 等
- 控制缓冲区 — 大文件操作指定合适的缓冲区大小(默认 4KB 太小)
- 路径安全 — 不要信任用户输入的路径,防止路径遍历攻击
关键知识点
- 先明确这个主题影响的是语法层、运行时层,还是性能与可维护性层。
- 学习时要同时关注语言表面写法和编译器、JIT、GC 等底层行为。
- 真正有价值的是知道"为什么这样写"和"在什么边界下不能这样写"。
项目落地视角
- 把示例改成最小可运行样例,并观察编译输出、运行结果和异常行为。
- 如果它会进入团队代码规范,最好同步补充命名约定、禁用场景和替代方案。
- 涉及性能结论时,优先用 Benchmark 或实际热点链路验证,而不是凭感觉判断。
常见误区
- 只记语法糖,不知道底层成本。
- 把适用于小样例的写法直接搬到高并发或大对象场景里。
- 忽略框架版本、语言版本和运行时差异,导致结论失真。
- 用 ReadAllText 读取大文件导致 OOM。
- 忘记 Dispose Stream 导致文件锁定。
- 路径拼接用字符串而不是 Path.Combine(跨平台问题)。
- FileSystemWatcher 不处理重复事件和事件丢失。
- 在高并发下多个线程同时写同一文件。
进阶路线
- 继续向源码、IL、JIT 行为和 BCL 实现层深入。
- 把知识点和代码评审、性能诊断、面试复盘结合起来。
- 把同类主题做横向对比,例如值类型与引用类型、迭代器与 async 状态机、反射与 Source Generator。
- 学习 System.IO.Pipelines 的高级用法。
- 了解 OS 级别的 I/O 模型(IOCP、epoll、io_uring)。
适用场景
- 当你准备把《文件 I/O 与流操作》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合在需要理解语言特性、运行时行为或 API 边界时阅读。
- 当代码开始出现性能瓶颈、可维护性问题或语义歧义时,这类主题会直接影响实现质量。
落地建议
- 先写最小可运行样例,再把结论迁移到真实业务代码。
- 同时记录这个特性的收益、限制和替代方案,避免为了"高级"而使用。
- 涉及内存、并发或序列化时,最好配合调试器或基准测试验证。
排错清单
- 先确认问题属于编译期、运行期还是语义误用。
- 检查是否存在隐式转换、装箱拆箱、闭包捕获或上下文切换等隐藏成本。
- 查看异常栈、日志和最小复现代码,优先排除使用姿势问题。
- 检查文件是否被其他进程锁定。
- 确认路径是否正确(跨平台分隔符)。
复盘问题
- 如果把《文件 I/O 与流操作》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《文件 I/O 与流操作》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《文件 I/O 与流操作》最大的收益和代价分别是什么?
