首页 / C# 入门教程 / 读写文本文件

C# 入门教程

读写文本文件

本教程共 100 篇 · 第 95 篇 · 更新于 2026-07-31 · 约 8 分钟阅读

C#C# 入门教程编程语言文件StreamReaderStreamWriter

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}");

六、指定编码(避免乱码)

FileStreamReader/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,乱码再换。

七、新手踩坑

  1. 读一个不存在的文件,File.ReadAllText 会抛 FileNotFoundException。读之前用 File.Exists(path) 判断,或 try-catch 兜底。
  2. 路径跨平台问题:别手拼分隔符,用 Path.Combine(见第 94 章)。
  3. 超大文件用 ReadAllText,内存直接拉满。换 StreamReader 逐行。
  4. StreamWriterusing 就结束,内容可能没真正落盘。
  5. ReadLine 时把判空写成 while (reader.ReadLine() != null) 会丢掉读到的内容;要像上面那样先赋值再判断。
Warning

WriteAllText 写入已有文件会「整篇覆盖」,原内容一去不回。如果是想补日志,请改用 AppendAllTextStreamWriter 的追加模式(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。只有当你改用 FileStreamStreamReaderStreamWriter 这类底层类型时,才需要自己用 using 管释放。分清「高层一次性方法」和「底层流」,就不会在释放上犯迷糊。

Tip

File 的静态方法每次调用都会打开、关闭一次文件句柄。如果你要在循环里反复对同一个文件读写,反复开关反而慢;这种场景不如直接用 StreamReader/StreamWriter 一次打开、多次操作,效率更高。

一次性读写还是流式处理

这是读文件最常被问的问题,一句话原则:文件小、要整篇处理,用 File 静态方法最省事;文件大、或要边读边处理,用 StreamReader/StreamWriter

具体怎么分:

  • 配置文件、模板、几 MB 以内的文本:直接 File.ReadAllText,代码最短,内部已帮你开流关流。
  • 日志、导出、几个 GB 的大文件:必须用流逐行处理,否则内存被一次性撑满,程序可能直接抛 OutOfMemoryException
  • 需要精细控制(只读前 N 行、跳过表头、边读边写新文件):流给你逐字节、逐行的控制权,想怎么切都行。
Tip

拿不准文件有多大时,先问自己「这文件会不会超过几十 MB」。会,就用流;不会,就用 File。这是简单又安全的经验线,不必纠结。

Note

换行符也值得留意:Windows 用 \r\n,Linux/macOS 用 \nReadAllLines 会自动按当前系统识别换行,跨平台读一般没问题;但如果你手动拼文本写文件,建议用 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/StreamWriterusing。下一章我们把「流」的概念讲透,并顺带看 JSON 序列化。