1. Вступ
Уявімо, що нам знову потрібно прочитати вміст текстового файлу. Припустімо, у нас є файл з корисними цитатами програмістів, які ми хочемо виводити у наш уже знайомий текстовий застосунок. Так, ми вже бачили високорівневі методи на кшталт File.ReadAllText, але що, якщо файл великий і нам потрібно читати його не повністю, а построково? Або хочемо читати файл контрольовано: наприклад, повільно підвантажувати рядки, щоб не перевантажувати памʼять?
Саме тут на допомогу приходить StreamReader — клас, який ідеально підходить для читання текстових файлів рядок за рядком, поблоково, символ за символом — коротко, гнучко й зручно.
Як працює StreamReader?
StreamReader — це клас із простору імен System.IO, який реалізує читання текстових даних із потоку: зазвичай це файл, але може бути й мережевий потік або памʼять. Він вміє читати дані в потрібному кодуванні, розбирає їх на символи та рядки, а вам повертає зручні типи даних: рядки та символи.
Якщо подати це схематично, маємо таке:
[ Файл (bytes) ] --(FileStream)--> [ StreamReader (розбирає коди символів) ] ---> [ Твій код (string, char) ]
- Файл (або інше джерело байтів): Повертає байти.
- 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.
Ще одна практична «підніжка» можлива, якщо файл оновлюється під час роботи програми (наприклад, лог-файл). Не завжди можна одразу побачити нові рядки, якщо файл використовується іншими процесами — тоді потрібно перечитувати файл заново або використовувати спеціальні методи, але про це поговоримо пізніше.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ