1. Введение
Давайте представим, что у нас снова стоит задача прочитать содержимое текстового файла. Допустим, у нас есть файл с полезными цитатами программистов, которые мы хотим выводить в наше уже хорошо знакомое текстовое приложение. Да, мы уже видели высокоуровневые методы типа File.ReadAllText, но что если файл большой, и нам нужно читать его не целиком, а построчно? Или мы хотим читать файл контролируемо, например, медленно подгружать строки, чтобы не улететь по памяти?
Именно тут приходит на помощь StreamReader — класс, который отлично подходит для чтения текстовых файлов построчно, поблочно, символ за символом — словом, гибко и удобно.
Как работает StreamReader?
StreamReader — это класс из пространства имён System.IO, который реализует чтение текстовых данных из потока (обычно это файл, но может быть и сетевой поток, и память). Он умеет читать данные в нужной кодировке, разбирает их в символы и строки, а вам возвращает удобные типы данных: строки и символы.
Если изобразить это в виде схемы, получится что-то вроде:
[ Файл (bytes) ] --(FileStream)--> [ StreamReader (разбирает коды символов) ] ---> [ Ваш код (string, char) ]
- File (или другой источник байт): Даёт нам байты.
- FileStream: Читает эти байты, как "канал".
- StreamReader: Превращает байты в символы и строки с учётом кодировки (по умолчанию — UTF-8).
Почему не всегда стоит пользоваться только File.ReadAllText?
На практике файлы бывают большими. Даже очень большими (например, логи или csv на миллион строк). Пытаться прочитать такие файлы целиком в память — всё равно что пытаться за раз съесть весь торт: вкусно, но опасно для здоровья.
StreamReader читает файл "по кусочкам". Он не грузит весь файл в память, а подгружает и отдаёт ваш запрос на следующую строку или символ только когда это нужно. Это экономно и очень удобно.
2. Чтение файла целиком построчно
Создадим файл quotes.txt с такими строками:
Код — это поэзия.
Дебаг — это детектив.
"Не работает" — лучший баг-репорт.
Теперь добавим код к уже начатому нами учебному приложению (пусть это будет простая консольная программа, которую мы постепенно развиваем). Поставим файл рядом с .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 declaration
С современными версиями 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.
Сравнение методов чтения
| Способ | Когда использовать | Преимущества | Недостатки |
|---|---|---|---|
|
Маленькие файлы | Одна строчка кода, быстро | Большой файл – много памяти |
|
Любые, особенно большие файлы | Чтение по строкам, мало памяти | Чуть больше кода, сложнее логику строить |
|
Маленькие/средние файлы | Гибко управлять кодировкой | Тяжело с очень большими файлами |
5. Практика
Давайте усложним задание. Пусть наша программа выводит только первые 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.
Ещё одна практическая "подножка" может быть, если файл обновляется по ходу работы программы (например, лог-файл). Не всегда можно сразу увидеть новые строки, если файл используется другими процессами — тогда нужно пересчитывать/перечитывать файл заново или использовать специальные методы, но об этом мы поговорим позднее.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ