读写文本文件
本教程共 100 篇 · 第 95 篇 · 更新于 2026-07-31 · 约 8 分钟阅读
95. 读写文本文件
本节目标:学完能用 File 类快速读写作业文本,并在大文件时用 StreamReader/StreamWriter 流式处理。
读配置、写日志、导报表——文本文件是日常最频繁的 IO 场景。C# 把简单活交给 File 静态类,把精细活交给 StreamReader/StreamWriter。
一、File 类:一行搞定整文件
如果只是小文件,整篇读、整篇写,File 的几个静态方法最直接。
namespace CSharpDemo;
string path = "note.txt";
// 写入(文件不存在则创建,存在则覆盖)
File.WriteAllText(path, "你好,世界!");
// 读回全部内容
string content = File.ReadAllText(path);
Console.WriteLine(content);
// 追加一行
File.AppendAllText(path, Environment.NewLine + "第二行内容");
Tip
WriteAllText会覆盖原内容;想追加用AppendAllText。二者都会自动建文件、用完自动关流,内部已经处理好资源释放,不必再包using。
二、按行读写
按行处理是最常见的需求:ReadAllLines 返回字符串数组,WriteAllLines 接收字符串集合(C# 12 起可用集合表达式 [..])。
namespace CSharpDemo;
File.WriteAllLines("lines.txt", ["第一行", "第二行", "第三行"]);
string[] lines = File.ReadAllLines("lines.txt");
foreach (var line in lines)
{
Console.WriteLine(line);
}
三、StreamReader:逐行读大文件
ReadAllText/ReadAllLines 会把整个文件塞进内存。文件几个 GB 时,内存就爆了。这时用 StreamReader 逐行读,读到一行处理一行。
namespace CSharpDemo;
using var reader = new StreamReader("lines.txt");
string? line;
while ((line = reader.ReadLine()) is not null)
{
Console.WriteLine($"读到:{line}");
}
Note
reader.ReadLine()返回string?:读到文件末尾返回null。用is not null同时完成「赋值 + 判空」,是 C# 14 下干净的写法。
四、StreamWriter:逐行写
对应地,StreamWriter 逐行写,配合 using 确保流被关闭、缓冲被刷盘。
namespace CSharpDemo;
using var writer = new StreamWriter("out.txt");
writer.WriteLine("第一行");
writer.WriteLine("第二行");
// 离开 using 作用域时自动 Flush 并释放
Tip忘了
using或没调Flush,最后几行可能留在缓冲区没写进磁盘。养成「凡是流,必 using」的习惯。
五、一次读全部到集合再处理
File.ReadAllLines 读成数组后可直接上 LINQ:
namespace CSharpDemo;
var nonEmpty = File.ReadAllLines("lines.txt")
.Where(l => l.Length > 0)
.ToList();
Console.WriteLine($"非空行数:{nonEmpty.Count}");
六、指定编码(避免乱码)
File 和 StreamReader/StreamWriter 默认用 UTF-8 读写。如果你的文件是 GB2312/GBK 等旧编码,必须显式指定,否则中文会乱码。
namespace CSharpDemo;
// .NET 默认不注册代码页编码,须先登记 CodePagesEncodingProvider
System.Text.Encoding.RegisterProvider(System.Text.Encoding.CodePages.CodePagesEncodingProvider.Instance);
using var reader = new StreamReader("legacy.txt", System.Text.Encoding.GetEncoding("GB2312"));
string text = reader.ReadToEnd();
Console.WriteLine(text);
Note.NET(Core)/ .NET 5+ 默认不注册 GB2312/GBK 等代码页编码,直接
GetEncoding("GB2312")会抛ArgumentException。须先引用System.Text.Encoding.CodePages包,并调用Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)注册一次,才能使用代码页编码。
Note现代项目尽量统一用 UTF-8,从源头消灭乱码。只有在读别人遗留的旧文件时,才需要按上面的方式指定
Encoding。不确定编码时,可先试 UTF-8,乱码再换。
七、新手踩坑
- 读一个不存在的文件,
File.ReadAllText会抛FileNotFoundException。读之前用File.Exists(path)判断,或 try-catch 兜底。 - 路径跨平台问题:别手拼分隔符,用
Path.Combine(见第 94 章)。 - 超大文件用
ReadAllText,内存直接拉满。换StreamReader逐行。 StreamWriter不using就结束,内容可能没真正落盘。- 用
ReadLine时把判空写成while (reader.ReadLine() != null)会丢掉读到的内容;要像上面那样先赋值再判断。
Warning用
WriteAllText写入已有文件会「整篇覆盖」,原内容一去不回。如果是想补日志,请改用AppendAllText或StreamWriter的追加模式(new StreamWriter(path, append: true)),别把重要数据一键清空。
补充:File 常用方法速查
File 静态类方法不多但够用,常碰到的有这些:
File.WriteAllText(path, text):整篇写入,覆盖原内容。File.AppendAllText(path, text):在末尾追加文本。File.ReadAllText(path):整篇读回一个字符串。File.WriteAllLines(path, lines):按行写入(数组或集合,C# 12 起可用[..])。File.ReadAllLines(path):按行读回字符串数组。File.AppendAllLines(path, lines):按行追加。File.ReadAllBytes(path)/File.WriteAllBytes(path, bytes):按字节读写(适合图片、压缩包)。File.Exists(path):判断文件是否存在,读之前先探一下更稳。File.Delete(path):删除文件,不可逆,慎用。File.Copy(src, dst):复制文件到新路径。
Note上面这些静态方法内部都自带
using逻辑,调用完自动释放句柄,所以单独用它们时不必再包using。只有当你改用FileStream、StreamReader、StreamWriter这类底层类型时,才需要自己用using管释放。分清「高层一次性方法」和「底层流」,就不会在释放上犯迷糊。
Tip
File的静态方法每次调用都会打开、关闭一次文件句柄。如果你要在循环里反复对同一个文件读写,反复开关反而慢;这种场景不如直接用StreamReader/StreamWriter一次打开、多次操作,效率更高。
一次性读写还是流式处理
这是读文件最常被问的问题,一句话原则:文件小、要整篇处理,用 File 静态方法最省事;文件大、或要边读边处理,用 StreamReader/StreamWriter。
具体怎么分:
- 配置文件、模板、几 MB 以内的文本:直接
File.ReadAllText,代码最短,内部已帮你开流关流。 - 日志、导出、几个 GB 的大文件:必须用流逐行处理,否则内存被一次性撑满,程序可能直接抛
OutOfMemoryException。 - 需要精细控制(只读前 N 行、跳过表头、边读边写新文件):流给你逐字节、逐行的控制权,想怎么切都行。
Tip拿不准文件有多大时,先问自己「这文件会不会超过几十 MB」。会,就用流;不会,就用
File。这是简单又安全的经验线,不必纠结。
Note换行符也值得留意:Windows 用
\r\n,Linux/macOS 用\n。ReadAllLines会自动按当前系统识别换行,跨平台读一般没问题;但如果你手动拼文本写文件,建议用Environment.NewLine而不是硬编码\n,保证在目标系统上换行正确,避免「在自己机器上正常、到别处就挤成一行」的尴尬。
编码为什么重要
「编码」决定字节怎么变成字符。UTF-8 是现代默认,一个中文字通常占 3 字节,且向后兼容 ASCII。老系统常用 GBK/GB2312,一个中文占 2 字节。如果你用 UTF-8 去读一个 GBK 写的文件,中文字节对不上,就会变成乱码(常见 ? 或一串陌生符号)。
所以:新项目一律 UTF-8;读别人遗留文件时,先确认它的编码,再用对应的 Encoding 打开。判断不出编码,可先后备试 UTF-8,再看输出是否正常。
Warning用
WriteAllText写文件时,默认是 UTF-8(无 BOM)。某些老工具(如旧版 Excel 读 CSV)认不出无 BOM 的 UTF-8,会显示乱码。如需兼容性,可显式传new UTF8Encoding(true)写入带 BOM 的文件,让老工具也能正确识别。
八、为什么这样设计
为什么 C# 要分「File 一把梭」和「StreamReader/StreamWriter 流式」两套?因为文件大小天差地别。小文件(配置、备注)一次性读写最省心,于是 File 静态方法把「开流、读写、关流」三步打包成一个调用。大文件(日志、导出)若也一次性读进内存,几 GB 直接把内存吃满,所以流(stream)把数据变成「一边读一边处理」的管道,内存占用恒定。
using 在这里是关键:流握着操作系统文件句柄,using 保证无论正常结束还是中途异常,句柄都会被释放。否则文件会被一直占用,别的进程删不掉也改不了。
九、File 静态方法 vs 流的对比
| 方式 | 适用 | 内存 | 是否需要 using |
|---|---|---|---|
File.WriteAllText/ReadAllText | 小文件、整篇读写 | 全量载入 | 内部已处理 |
File.ReadAllLines/WriteAllLines | 小文件、按行 | 全量载入 | 内部已处理 |
StreamReader/StreamWriter | 大文件、流式处理 | 恒定(逐行) | 必须 using |
一句话:小文件图省事用 File,大文件或要精细加工用流,永远别让文件句柄「忘了关」。
十、本节小结
小文件用 File.WriteAllText/ReadAllText 最省事;大文件或需逐行加工,用 StreamReader/StreamWriter 配 using。下一章我们把「流」的概念讲透,并顺带看 JSON 序列化。