C# - 文件I/O
C# - 现代文件 I/O
Section titled “C# - 现代文件 I/O”文件输入/输出(I/O)是大多数应用程序的基本组成部分。它涉及从存储设备上的文件读取数据以及向文件写入数据。在 .NET 中,这些操作主要由 System.IO 命名空间中的类管理。
当文件被打开时,它被表示为一个流(stream),即字节序列。**输入流(input stream)从源(例如文件)读取数据,而输出流(output stream)**将数据写入目标。
System.IO 中的关键类
Section titled “System.IO 中的关键类”System.IO 命名空间提供了丰富的文件和目录操作工具集。以下是现代开发中一些最重要的类:
| Class | Primary Use Case |
|---|---|
File | 提供静态方法,用于快速简单的文件操作,如读取所有文本、写入所有行、复制和删除。 |
Directory | 提供静态方法,用于创建、移动和枚举目录及子目录。 |
Path | 提供处理目录和文件路径字符串的方法,以实现跨平台(例如 Path.Combine)。 |
StreamReader | 从字节流中读取字符,非常适合逐行读取文本文件。 |
StreamWriter | 向流中写入字符,非常适合写入文本文件。 |
FileStream | 一个低级类,为文件提供流,支持同步和异步读写操作,并提供细粒度控制。 |
基本最佳实践:使用 using 进行资源管理
Section titled “基本最佳实践:使用 using 进行资源管理”文件句柄是必须妥善关闭的非托管资源,以防止内存泄漏和文件锁定问题。using 语句(或较新的 using 声明)是确保 IDisposable 对象(如流)即使在发生异常时也能自动关闭和释放的标准且最安全的方式。
// C# 8.0 及更高版本的 'using' 声明// 'writer' 在当前作用域(Main 方法)结束时被释放。using var writer = new StreamWriter("log.txt");writer.WriteLine("Log entry.");
// 传统的 'using' 语句// 'reader' 在 using 块结束时被释放。using (var reader = new StreamReader("log.txt")){ string? line = reader.ReadLine(); Console.WriteLine(line);}使用 File 类进行简单的文件操作
Section titled “使用 File 类进行简单的文件操作”对于许多常见任务,File 类的静态方法是最简单有效的方法。
// 使用 Path.Combine 确保跨平台路径安全string filePath = Path.Combine(Directory.GetCurrentDirectory(), "mydata.txt");
// 1. 将内容写入文件(创建或覆盖)string[] content = { "First line", "Second line", "Third line" };File.WriteAllLines(filePath, content);Console.WriteLine("File created and content written.");
// 2. 将内容追加到现有文件File.AppendAllText(filePath, "This is an appended line.\n");Console.WriteLine("Content appended.");
// 3. 读取文件的所有内容string fileContent = File.ReadAllText(filePath);Console.WriteLine("\n--- File Content ---");Console.WriteLine(fileContent);
// 4. 检查文件是否存在并删除它if (File.Exists(filePath)){ File.Delete(filePath); Console.WriteLine("\nFile deleted successfully.");}使用 async 和 await 进行异步文件 I/O
Section titled “使用 async 和 await 进行异步文件 I/O”在现代应用程序(尤其是 UI、Web 和服务器应用程序)中,异步执行 I/O 操作以避免阻塞线程至关重要。这可以提高可伸缩性和响应能力。async 和 await 关键字使这变得简单明了。
示例:异步读取文件
Section titled “示例:异步读取文件”// 此方法必须从异步上下文调用,例如 'await ReadFileAsync()'。async Task ReadFileAsync(string filePath){ try { // 为示例创建一个虚拟文件 await File.WriteAllTextAsync(filePath, "This is an async example.");
Console.WriteLine("Reading file asynchronously..."); string content = await File.ReadAllTextAsync(filePath); Console.WriteLine($"File content: {content}");
// 清理虚拟文件 File.Delete(filePath); } catch (IOException ex) { Console.WriteLine($"An error occurred: {ex.Message}"); }}
// 要运行此代码,你需要一个异步的 Main 方法:// static async Task Main(string[] args) { ... }await ReadFileAsync("async_test.txt");使用 FileStream 获取更多控制
Section titled “使用 FileStream 获取更多控制”虽然 File 类非常适合简单的操作,但 FileStream 为你提供了对文件访问的细粒度控制,例如指定文件模式、访问权限和共享选项。它通常用于处理二进制文件或你希望分块处理的超大文件。
示例:读写二进制数据
Section titled “示例:读写二进制数据”string binaryFilePath = "test.dat";
// 使用 'FileMode.Create' 来创建新文件或覆盖现有文件。using (FileStream fs = new FileStream(binaryFilePath, FileMode.Create, FileAccess.Write)){ // BinaryWriter 使写入原始类型变得容易。 using (var writer = new BinaryWriter(fs)) { writer.Write(123); // int writer.Write(3.14159); // double writer.Write("Hello, Binary!"); // string }}
// 使用 'FileMode.Open' 从现有文件读取。using (FileStream fs = new FileStream(binaryFilePath, FileMode.Open, FileAccess.Read)){ // BinaryReader 以相同的顺序读回数据。 using (var reader = new BinaryReader(fs)) { int myInt = reader.ReadInt32(); double myDouble = reader.ReadDouble(); string myString = reader.ReadString();
Console.WriteLine($"Read Integer: {myInt}"); Console.WriteLine($"Read Double: {myDouble}"); Console.WriteLine($"Read String: {myString}"); }}安全与实践考量
Section titled “安全与实践考量”- 路径遍历:切勿直接拼接用户输入来构造文件路径。恶意用户可能会提供类似
../../secrets.txt的输入。始终使用Path.Combine()来正确处理路径分隔符,并验证用户输入。 - 错误处理:文件操作可能因多种原因失败(
FileNotFoundException、UnauthorizedAccessException等)。始终将 I/O 代码包装在try-catch块中,以优雅地处理潜在的IOException及其派生类。 - 处理 JSON/XML:一个非常常见的用例是将对象序列化为 JSON 或 XML 文件。
System.Text.Json(用于 JSON)和System.Xml.Serialization等库可以与文件流无缝集成以实现此目的。 - 大文件:处理超大文件时,应避免使用
File.ReadAllBytes()等将整个文件加载到内存中的方法。相反,应使用FileStream以更小、更易于管理的块读取文件。