Skip to content

C# - 文件I/O

文件输入/输出(I/O)是大多数应用程序的基本组成部分。它涉及从存储设备上的文件读取数据以及向文件写入数据。在 .NET 中,这些操作主要由 System.IO 命名空间中的类管理。

当文件被打开时,它被表示为一个流(stream),即字节序列。**输入流(input stream)从源(例如文件)读取数据,而输出流(output stream)**将数据写入目标。

System.IO 命名空间提供了丰富的文件和目录操作工具集。以下是现代开发中一些最重要的类:

ClassPrimary 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 关键字使这变得简单明了。

// 此方法必须从异步上下文调用,例如 '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");

虽然 File 类非常适合简单的操作,但 FileStream 为你提供了对文件访问的细粒度控制,例如指定文件模式、访问权限和共享选项。它通常用于处理二进制文件或你希望分块处理的超大文件。

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}");
}
}
  • 路径遍历:切勿直接拼接用户输入来构造文件路径。恶意用户可能会提供类似 ../../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 以更小、更易于管理的块读取文件。