CodeGym /课程 /C# SELF /读取文本文件: StreamReader...

读取文本文件: StreamReader

C# SELF
第 36 级 , 课程 2
可用

1. 入门

想象一下,我们又要读取一个文本文件的内容了。比如说,我们有个文件,里面是程序员的金句,想把它们输出到我们熟悉的文本应用里。是的,我们已经见过像File.ReadAllText这种高级方法,但如果文件很大,我们不想一次性全读进内存,而是想一行一行慢慢来?或者我们想更可控地读取,比如慢慢加载,别让内存爆掉?

这时候StreamReader就派上用场了——它超适合一行一行、块状、甚至一个字符一个字符地读文本文件,灵活又方便。

StreamReader是怎么工作的?

StreamReaderSystem.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.CombineAppDomain.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"));

编码和Encoding文档

把整个文件读成一个大字符串

有时候想一次性拿到整个文件内容,但不用File.ReadAllText,而是用StreamReader


using StreamReader reader = new StreamReader(filePath);
string allText = reader.ReadToEnd();
Console.WriteLine(allText);
  • ReadToEnd()方法会从当前位置一直读到文件结尾,返回一个大字符串。
  • 大文件这样读可能会很慢,还可能OutOfMemoryException。几兆的小文件用这个方法没问题。

实际生活里的用法

  • 日志处理。 有服务器日志文件时,一行一行读,过滤需要的事件,不用全塞进内存。
  • CSV导入。 解析大表格时,一行一行读数据,边读边处理。
  • 在大文本文件里查关键字。 可以在百万行里找内容,不怕内存爆掉。
  • 单元测试。 测试数据文件经常用StreamReader一行一行读。

读取方法对比

方式 啥时候用 优点 缺点
File.ReadAllText
小文件 一行代码,快 大文件——内存爆炸
StreamReader.ReadLine()
任何文件,尤其大文件 一行一行读,省内存 代码多点,逻辑稍复杂
StreamReader.ReadToEnd()
小/中等文件 编码可控 超大文件不行

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构造函数有第二个参数就是干这个用的。

还有个实际“坑”:如果文件在程序运行时被别的进程更新(比如日志文件),你不一定能马上读到新内容——这时要重新读文件或者用特殊方法,这个以后再聊。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION