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。