Menu

C# 读写文件:File、StreamWriter 与 StreamReader

如何在 C# 中写入文件和读取文件:用 File.WriteAllText、ReadAllText、AppendAllText、WriteAllLines 和 ReadAllLines 处理整个文件,用 StreamWriter 和 StreamReader 处理大文件,用 Path.Combine 拼接路径,以及目录、编码和文件操作必须处理的异常。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

System.IO 命名空间提供了处理文件所需的一切。大多数任务只需调用一次静态类 File 的方法:它会打开文件、读取或写入,然后关闭文件。处理大文件或需要更精细的控制时,使用 StreamReader 和 StreamWriter。下面每个示例都要加上 using System.IO;。

写入和读取整个文件

File.WriteAllText 用你给出的文本创建一个文件(或覆盖已有文件)。File.ReadAllText 把整个文件作为一个字符串返回。File.AppendAllText 则在末尾追加内容:

输出:

True
Buy coffee
Call the dentist
Renew passport
43 characters
Start over
False

像 "notes.txt" 这样的相对路径是相对于进程的当前目录解析的,而当前目录并不总是程序所在的文件夹(服务、计划任务或测试运行器可能从别的位置启动)。对于随应用一起发布的文件,请基于 AppContext.BaseDirectory 构建路径。

WriteAllText 会直接覆盖,不会询问。如果丢失旧内容会造成问题,先用 File.Exists 检查,或者先写入一个临时文件,再用 File.Move 把它移到目标位置。

按行处理:WriteAllLines、ReadAllLines 和 ReadLines

每行一条记录的文件非常常见,因此有专门的方法。WriteAllLines 写入每个字符串并在后面加上换行符;ReadAllLines 返回一个 string[]:

输出:

4 lines, header: date,product,amount
Total: 45.60
2026-03-03,Stapler,7.00

两种读取方式的区别在文件很大时才显现出来。ReadAllLines 在返回之前会把整个文件读入内存。ReadLines 返回一个惰性的 IEnumerable<string>,在你迭代时才读取,所以对一个 5 GB 的日志做 foreach 只占用几 KB 内存。配合 LINQ,ReadLines(...).Where(...).Take(10) 在找到十个匹配后就会停止读取。

用 Split(',') 解析 CSV 适用于你自己生成的简单文件。真实世界中的 CSV 会有包含逗号的带引号字段,这种情况请使用 CsvHelper 之类的库。

注意 decimal.Parse 中的 InvariantCulture:没有它,在设置为德语或葡萄牙语的机器上解析 "4.50" 时,点号会被当作千位分隔符。

StreamWriter 和 StreamReader

File 的方法每次调用都会打开并关闭文件。如果要分多次写入很多内容,或者不借助 LINQ 读取一个大到无法放入内存的文件,就只打开一次流。始终把它包在 using 中,这样即使抛出异常也能关闭它;没有关闭的写入器可能永远不会刷新缓冲区,文件最后会是空的或被截断。

输出:

2: WARN  disk 85% full
4: ERROR connection refused

ReadLine 在文件末尾返回 null,while 循环正是因此结束。StreamWriter 会缓冲输出,在缓冲区写满、你调用 Flush() 以及它被释放时写入磁盘;using 块保证了最后这一次。

从 C# 8 开始,using 声明会在所在代码块结束时释放流,不需要额外的花括号:

using var writer = new StreamWriter(path);   // C# 8: disposed when the method returns
writer.WriteLine("INFO  server started");

路径和目录

用 Path.Combine 构建路径,而不是拼接字符串。它会为操作系统插入正确的分隔符(Windows 上是 \,Linux 和 macOS 上是 /),并且不会重复。Path 类还能拆分路径,Directory 则用来创建和列出文件夹:

输出:

february.txt, january.txt, march.txt
march.txt
march
.txt
march.pdf
False

有一个陷阱:如果 Path.Combine 后面的某个参数是根路径(以 / 或 \ 开头,或在 Windows 上以盘符开头),它之前的所有部分都会被丢弃,所以 Path.Combine("reports", "/2026") 的结果是 /2026。请传入相对路径部分,或者使用 Path.Join(.NET Core 3.0 及以上),它从不丢弃任何部分。

Directory.GetFiles 按文件系统给出的顺序返回文件,Windows 和 Linux 上的顺序不同,所以在意顺序时要对结果排序。Directory.EnumerateFiles 是它的惰性版本,两者的关系与 ReadLines 和 ReadAllLines 相同。传入 SearchOption.AllDirectories 可以包含子文件夹。

处理错误

文件操作会因为程序之外的原因失败:文件不存在、文件夹不存在、另一个进程正打开着它、磁盘已满、权限不对。每种情况都有自己的异常类型。FileNotFoundException 和 DirectoryNotFoundException 派生自 IOException,所以要在它之前捕获;UnauthorizedAccessException 不是,所以它需要单独的 catch:

输出:

missing file, using defaults
missing folder, using defaults
theme=dark

在读取前调用 File.Exists 看起来更简单,但它并不能省掉 try:在检查和读取之间的那一刻,文件可能消失,或被另一个程序锁定。用 Exists 决定要做什么,用 catch 应对实际发生的情况。

编码

File.WriteAllText、WriteAllLines 和 StreamWriter 默认写入不带字节顺序标记(BOM)的 UTF-8,读取方法如果发现字节顺序标记,会据此识别 UTF-8、UTF-16 和 UTF-32。当文件必须使用特定格式时,传入一个 Encoding:

输出:

plain.txt   16 bytes, reads back unchanged: True
bom.txt     19 bytes, reads back unchanged: True
utf16.txt   30 bytes, reads back unchanged: True

é 和 ã 在 UTF-8 中各占两个字节,所以 14 个字符需要 16 个字节;三个文件都能正确读回,因为读取方法能识别字节顺序标记。Encoding.UTF8 看起来像默认值,但它会加上字节顺序标记,有些工具(较老的 CSV 导入工具、shell 脚本)会把它显示成第一行开头的乱码;要明确使用不带 BOM 的 UTF-8,请用 new UTF8Encoding(false)。用错误的编码读取文件不会抛出异常,只会产生替换字符,所以对于不是你创建的文件,先弄清楚它用的是什么编码。

异步文件 I/O

在 Web 应用和 UI 代码中,让线程阻塞在磁盘 I/O 上是一种浪费。.NET Core 2.0 及以上版本提供了 File 方法的异步版本:

await File.WriteAllTextAsync("notes.txt", text);
string content = await File.ReadAllTextAsync("notes.txt");
string[] lines = await File.ReadAllLinesAsync("sales.csv");

StreamReader.ReadLineAsync 和 StreamWriter.WriteLineAsync 在流上的用法相同。

常见错误

  • 没有释放流。 不用 using 的话,缓冲的文本可能永远写不到磁盘,而且在垃圾回收器运行之前文件一直被锁定。
  • 用 + "\\" + 拼接路径。 这在 Linux 和 macOS 上会出错。请使用 Path.Combine。
  • 用 ReadAllText 或 ReadAllLines 加载超大文件。 请用 ReadLines 或 StreamReader 以流的方式读取。
  • 依赖当前目录。 它取决于程序是如何启动的。请使用绝对路径或 AppContext.BaseDirectory。
  • 用本机区域设置解析文件中的数字。 处理数据文件时请传入 CultureInfo.InvariantCulture。
  • 指望 File.Exists 能防止异常。 文件在检查和使用之间可能发生变化。

常见问题

C# 中如何把文本写入文件?

File.WriteAllText("notes.txt", text); 会创建文件(文件已存在时则覆盖),以 UTF-8 写入字符串,然后关闭文件。要写入一组行,用 File.WriteAllLines(path, lines);要追加到末尾而不是替换内容,用 File.AppendAllText(path, text)。这三个方法都在 System.IO 中。

C# 中如何读取文本文件?

string text = File.ReadAllText("notes.txt"); 把整个文件读成一个字符串,string[] lines = File.ReadAllLines(path); 则把它拆成多行。大文件请在 foreach 中使用 File.ReadLines(path),它每次只读一行,不会一次加载全部内容;也可以用 StreamReader。

C# 中如何向文件追加内容?

File.AppendAllText(path, text) 把文本添加到文件末尾,文件不存在时会先创建它;File.AppendAllLines(path, lines) 对多行做同样的事。使用流时,以追加模式打开:new StreamWriter(path, append: true)。追加文本时记得自己加上 Environment.NewLine 或 \n。

C# 中如何逐行读取文件?

foreach (string line in File.ReadLines(path)) { ... } 每次只读取一行,所以即使是 GB 级的日志,内存占用也保持平稳。较老的等价写法是在 using 块中使用 StreamReader,配合 while ((line = reader.ReadLine()) != null)。大文件不要用 ReadAllLines:它会先把所有行加载进一个数组。

C# 中如何判断文件是否存在?

如果文件存在且程序有权看到它,File.Exists(path) 返回 true;Directory.Exists(path) 对文件夹做同样的判断。不要指望靠它避免异常:在检查和读取之间,文件可能被删除或锁定,所以实际进行文件操作时仍要处理 FileNotFoundException 和 IOException。

Coddy 编程语言插图

和 Coddy 一起学编程

开始