1. 入门
想象一下,我们又要读取一个文本文件的内容了。比如说,我们有个文件,里面是程序员的金句,想把它们输出到我们熟悉的文本应用里。是的,我们已经见过像File.ReadAllText这种高级方法,但如果文件很大,我们不想一次性全读进内存,而是想一行一行慢慢来?或者我们想更可控地读取,比如慢慢加载,别让内存爆掉?
这时候StreamReader就派上用场了——它超适合一行一行、块状、甚至一个字符一个字符地读文本文件,灵活又方便。
StreamReader是怎么工作的?
StreamReader是System.IO命名空间下的一个类,专门用来从流里读文本数据(通常是文件,也可以是网络流、内存流啥的)。它能用指定编码读数据,把它们解析成字符和字符串,然后返回给你用的类型:string和char。
如果画个流程图,大概是这样:
[ 文件 (bytes) ] --(FileStream)--> [ StreamReader (解析字符编码) ] ---> [ 你的代码 (string, char) ]
- 文件(或其他字节来源): 给我们字节。
- FileStream: 把这些字节当“通道”读出来。
- StreamReader: 按编码把字节变成字符和字符串(默认UTF-8)。
为啥不总用File.ReadAllText?
实际开发里,文件有时候很大,甚至超级大(比如日志、百万行的csv)。一次性全读进内存就像一次吃掉整个蛋糕:虽然爽,但对健康有害。
StreamReader是“分块”读文件的。它不会把整个文件都塞进内存,而是按需加载,只有你要下一行或下一个字符时才读。这很省资源,也很方便。
2. 整个文件一行一行读
我们先造个quotes.txt文件,内容如下:
代码——就是诗。
调试——就是侦探。
“不工作”——最棒的bug报告。
现在把代码加到我们已经写了一半的学习小程序里(就当是个普通控制台程序,慢慢完善)。把文件放在.exe旁边——让程序一行一行读出来显示。
带注释的代码:
// 获取程序目录下的文件路径
string fileName = "quotes.txt";
string filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);
// 检查文件是否存在
if (!File.Exists(filePath))
{
Console.WriteLine($"文件未找到: {filePath}");
return;
}
// 用StreamReader打开文件读
using StreamReader reader = new StreamReader(filePath);
string? line;
int lineNumber = 1;
// 一行一行读到结尾
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine($"{lineNumber,2}: {line}");
lineNumber++;
}
这里发生了啥?
- 我们用Path.Combine和AppDomain.CurrentDomain.BaseDirectory拼出文件路径——这样Windows、Linux、甚至Docker里都能跑。
- 先检查文件真的存在。
- 用using创建StreamReader,一行一行读(ReadLine()方法)。
- using块结束时自动关文件,不管你忘了手动关还是遇到异常。
可视化:工作流程图
[ quotes.txt ] --(FileStream)--> [ StreamReader ] --(ReadLine)--> [ string line ] --(Console.WriteLine)--> 屏幕
^
|
AppDomain.CurrentDomain.BaseDirectory + Path.Combine
3. 使用细节和注意点
怎么读一行
ReadLine()方法会返回到第一个换行符(\n或\r\n)前的字符串。文件读完时返回null。所以循环一般这么写:
string? line;
while ((line = reader.ReadLine()) != null)
{
// 处理每一行
}
用StreamReader常见的坑
有时候会偷懒不用using,直接写:
StreamReader reader = new StreamReader(filePath);
// ...
reader.Close();
这种写法就像冬天忘了关门:啥都可能发生。如果读文件时出错(比如磁盘突然断了),关文件的代码就不会执行,文件会一直被系统占着。
所以用using不是建议,是必须。你未来的运维同事会感谢你,程序不会留下“僵尸”文件。
using声明的简写
C#新版本(8.0起)可以更简洁地写:
using StreamReader reader = new StreamReader(filePath);
string? line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine(line);
}
Dispose会在当前块(方法)结束时自动调用。
4. 实用细节
读取未知编码的文件
默认StreamReader用UTF-8。但有时候文件是别的编码(比如老的俄语文本用Windows-1251)。这时程序可能会输出“?”或者乱码。
可以手动指定编码:
using System.Text;
// 用Windows-1251打开文件
using StreamReader reader = new StreamReader(filePath, Encoding.GetEncoding("windows-1251"));
把整个文件读成一个大字符串
有时候想一次性拿到整个文件内容,但不用File.ReadAllText,而是用StreamReader。
using StreamReader reader = new StreamReader(filePath);
string allText = reader.ReadToEnd();
Console.WriteLine(allText);
- ReadToEnd()方法会从当前位置一直读到文件结尾,返回一个大字符串。
- 大文件这样读可能会很慢,还可能OutOfMemoryException。几兆的小文件用这个方法没问题。
实际生活里的用法
- 日志处理。 有服务器日志文件时,一行一行读,过滤需要的事件,不用全塞进内存。
- CSV导入。 解析大表格时,一行一行读数据,边读边处理。
- 在大文本文件里查关键字。 可以在百万行里找内容,不怕内存爆掉。
- 单元测试。 测试数据文件经常用StreamReader一行一行读。
读取方法对比
| 方式 | 啥时候用 | 优点 | 缺点 |
|---|---|---|---|
|
小文件 | 一行代码,快 | 大文件——内存爆炸 |
|
任何文件,尤其大文件 | 一行一行读,省内存 | 代码多点,逻辑稍复杂 |
|
小/中等文件 | 编码可控 | 超大文件不行 |
5. 实践
来点进阶。让程序只输出文件的前N行,N由用户输入。有时候不想看几百行,来两句金句就够激励了 :)
实现如下:
using System;
using System.IO;
class Program
{
static void Main()
{
string fileName = "quotes.txt";
string filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName);
if (!File.Exists(filePath))
{
Console.WriteLine($"文件未找到: {filePath}");
return;
}
Console.Write("输出几行?");
string? input = Console.ReadLine();
if (!int.TryParse(input, out int linesToShow) || linesToShow < 1)
{
Console.WriteLine("错误:请输入大于0的正确数字。");
return;
}
using StreamReader reader = new StreamReader(filePath);
int current = 0;
string? line;
while (current < linesToShow && (line = reader.ReadLine()) != null)
{
Console.WriteLine(line);
current++;
}
if (current == 0)
Console.WriteLine("文件为空或第一行没读到。");
else if (current < linesToShow)
Console.WriteLine($"文件里只有 {current} 行。");
}
}
6. 重要提示和常见坑
用StreamReader时,最重要的是资源管理要对(参考前一节关于IDisposable的内容)。忘了这点就会遇到“文件已被其他进程占用”或“打开文件太多”的报错。
还有个细节——编码要小心。如果不确定文件是不是UTF-8,最好手动指定编码。StreamReader构造函数有第二个参数就是干这个用的。
还有个实际“坑”:如果文件在程序运行时被别的进程更新(比如日志文件),你不一定能马上读到新内容——这时要重新读文件或者用特殊方法,这个以后再聊。
GO TO FULL VERSION