У попередньому розділі ми навчилися працювати з файлами за допомогою класів File, Directory та їхніх об'єктно-орієнтованих аналогів. Але що робити, коли файл занадто великий для завантаження у пам'ять? Як читати дані з мережі або писати у стиснутий архів? Відповідь — Потоки (Streams).
Stream — це абстракція послідовності байтів. Уявіть потік води: ви можете черпати з нього воду порціями (читання), додавати нову воду (запис), або перекривати його (закриття). Аналогічно працюють потоки даних у C#.
🎯 Ефективність пам'яті
🌐 Універсальність
⚡ Асинхронність
🔗 Композиція
У C# всі потоки наслідуються від абстрактного класу System.IO.Stream. Це базовий клас, який визначає загальний інтерфейс для читання та запису.
FileStream у режимі FileMode.Create не може читати.false.NetworkStream не підтримує seek, бо це послідовний потік.CanSeek.CanSeek == true).count байтів у масив buffer починаючи з позиції offset. Повертає кількість фактично прочитаних байтів (може бути менше count!).count байтів з масиву buffer (починаючи з offset) у потік.SeekOrigin може бути: Begin (від початку), Current (від поточної позиції), End (від кінця).Read().Write().using statement. Незакриті потоки можуть заблокувати файли, витікати пам'ять або втратити дані через незаписаний буфер.FileStream — це найбільш низькорівневий спосіб роботи з файлами. На відміну від File.ReadAllText(), він дозволяє контролювати як саме читаються та записуються дані.
FileStream stream = new FileStream(
path: "data.bin",
mode: FileMode.OpenOrCreate,
access: FileAccess.ReadWrite,
share: FileShare.None,
bufferSize: 4096,
useAsync: true
);
| FileMode | Опис | Якщо файл існує | Якщо файл НЕ існує |
|---|---|---|---|
CreateNew | Створює новий файл | Викидає виняток | Створює |
Create | Створює файл | Перезаписує | Створює |
Open | Відкриває існуючий | Відкриває | Викидає виняток |
OpenOrCreate | Відкриває або створює | Відкриває | Створює |
Truncate | Відкриває та очищає | Очищає вміст | Викидає виняток |
Append | Додає у кінець | Позиція = кінець | Створює |
FileAccess.Read — тільки читанняFileAccess.Write — тільки записFileAccess.ReadWrite — читання та записFileShare.None — ексклюзивний доступ (інші процеси не можуть відкрити файл)FileShare.Read — інші можуть читатиFileShare.Write — інші можуть писатиFileShare.ReadWrite — інші можуть читати та писатиusing System;
using System.IO;
using System.Text;
class FileStreamDemo
{
static void Main()
{
string filePath = "message.bin";
// ЗАПИС
using (FileStream writeStream = new FileStream(
filePath,
FileMode.Create,
FileAccess.Write))
{
string message = "Привіт, FileStream!";
byte[] messageBytes = Encoding.UTF8.GetBytes(message);
// Записуємо довжину повідомлення (4 байти)
byte[] lengthBytes = BitConverter.GetBytes(messageBytes.Length);
writeStream.Write(lengthBytes, 0, lengthBytes.Length);
// Записуємо саме повідомлення
writeStream.Write(messageBytes, 0, messageBytes.Length);
Console.WriteLine($"Записано {messageBytes.Length} байт");
} // using автоматично викликає Dispose() → Close()
// ЧИТАННЯ
using (FileStream readStream = new FileStream(
filePath,
FileMode.Open,
FileAccess.Read))
{
// Читаємо довжину
byte[] lengthBytes = new byte[4];
readStream.Read(lengthBytes, 0, 4);
int messageLength = BitConverter.ToInt32(lengthBytes, 0);
// Читаємо повідомлення
byte[] messageBytes = new byte[messageLength];
readStream.Read(messageBytes, 0, messageLength);
string message = Encoding.UTF8.GetString(messageBytes);
Console.WriteLine($"Прочитано: {message}");
}
}
}
Розбір коду:
FileStream для запису. FileMode.Create перезапише файл, якщо він існує.Encoding.UTF8.using statement автоматично викликає Dispose(), який закриває потік.int за допомогою BitConverter.Уявімо, що маємо лог-файл розміром 2 ГБ. Завантажити його у пам'ять за допомогою File.ReadAllText() неможливо. Використаємо потокове читання.
using System;
using System.IO;
class LargeFileReader
{
static void Main()
{
string logPath = "huge_log.txt";
// Створюємо демонстраційний файл (багато рядків)
CreateDemoLog(logPath, lines: 1_000_000);
Console.WriteLine("Пошук помилок у великому файлі...\n");
int errorCount = 0;
int chunkSize = 4096; // 4 KB
byte[] buffer = new byte[chunkSize];
using (FileStream fs = new FileStream(logPath, FileMode.Open, FileAccess.Read))
{
int bytesRead;
long totalBytes = 0;
// Читаємо файл частинами
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
totalBytes += bytesRead;
// Конвертуємо chunk у рядок
string chunk = System.Text.Encoding.UTF8.GetString(buffer, 0, bytesRead);
// Рахуємо помилки
errorCount += CountOccurrences(chunk, "ERROR");
// Прогрес (кожні 10 MB)
if (totalBytes % (10 * 1024 * 1024) == 0)
{
Console.WriteLine($"Оброблено: {totalBytes / 1024 / 1024} MB");
}
}
}
Console.WriteLine($"\n✅ Знайдено помилок: {errorCount}");
Console.WriteLine($"Розмір файлу: {new FileInfo(logPath).Length / 1024 / 1024} MB");
}
static void CreateDemoLog(string path, int lines)
{
using (StreamWriter writer = new StreamWriter(path))
{
var random = new Random();
for (int i = 0; i < lines; i++)
{
string level = random.Next(10) == 0 ? "ERROR" : "INFO";
writer.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [{level}] Message #{i}");
}
}
}
static int CountOccurrences(string text, string substring)
{
int count = 0;
int index = 0;
while ((index = text.IndexOf(substring, index)) != -1)
{
count++;
index += substring.Length;
}
return count;
}
}
Розбір коду:
Read() повертає кількість фактично прочитаних байтів.while ((bytesRead = fs.Read(...)) > 0) — стандартний паттерн для потокового читання. Коли досягнемо кінця файлу, Read() поверне 0.MemoryStream зберігає дані у пам'яті (RAM) замість файлу. Це корисно для:
using System;
using System.IO;
using System.Text;
class MemoryStreamDemo
{
static void Main()
{
// Створюємо пустий MemoryStream
using (MemoryStream ms = new MemoryStream())
{
// Записуємо дані
string message = "Дані у пам'яті";
byte[] messageBytes = Encoding.UTF8.GetBytes(message);
ms.Write(messageBytes, 0, messageBytes.Length);
Console.WriteLine($"Позиція після запису: {ms.Position}");
Console.WriteLine($"Довжина потоку: {ms.Length}");
// Повертаємося на початок для читання
ms.Position = 0;
// Читаємо дані
byte[] buffer = new byte[ms.Length];
ms.Read(buffer, 0, buffer.Length);
string readMessage = Encoding.UTF8.GetString(buffer);
Console.WriteLine($"Прочитано: {readMessage}");
// Отримуємо масив байтів
byte[] allData = ms.ToArray();
Console.WriteLine($"ToArray() повернув {allData.Length} байт");
}
// Створюємо MemoryStream з існуючого масиву
byte[] existingData = Encoding.UTF8.GetBytes("Попередні дані");
using (MemoryStream ms2 = new MemoryStream(existingData))
{
ms2.Position = ms2.Length; // Переходимо в кінець
// Додаємо нові дані
byte[] newData = Encoding.UTF8.GetBytes(" + нові дані");
ms2.Write(newData, 0, newData.Length);
ms2.Position = 0; // Повертаємося на початок
byte[] combined = ms2.ToArray();
Console.WriteLine($"\nОб'єднані дані: {Encoding.UTF8.GetString(combined)}");
}
}
}
Розбір коду:
MemoryStream. Початкова ємність — 0, але автоматично зростає.ToArray() створює копію внутрішнього буфера. Для великих даних це може бути неефективно.MemoryStream існуючим масивом.GetBuffer() повертає внутрішній масив без копіювання, але він може містити невикористані байти. ToArray() повертає точну копію даних. Використовуйте GetBuffer() для продуктивності, якщо розумієте ризики.Робота з байтами незручна для текстових даних. StreamReader та StreamWriter — це обгортки над Stream, які працюють з рядками та символами замість байтів.
using System;
using System.IO;
class StreamReaderDemo
{
static void Main()
{
string filePath = "diary.txt";
// Створюємо демонстраційний файл
File.WriteAllLines(filePath, new[]
{
"2026-01-10: Вивчаю C# Streams",
"2026-01-11: Практикую async I/O",
"2026-01-12: Будую реальний проєкт"
});
// Читання всього файлу
using (StreamReader reader = new StreamReader(filePath))
{
string content = reader.ReadToEnd();
Console.WriteLine("=== ВЕСЬ ВМІСТ ===");
Console.WriteLine(content);
}
// Читання по рядках
Console.WriteLine("\n=== ПОРЯДКОВЕ ЧИТАННЯ ===");
using (StreamReader reader = new StreamReader(filePath))
{
string line;
int lineNumber = 1;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine($"[{lineNumber}] {line}");
lineNumber++;
}
}
// Читання з кодуванням
using (StreamReader reader = new StreamReader(filePath, System.Text.Encoding.UTF8))
{
Console.WriteLine($"\nПерший символ: '{(char)reader.Read()}'");
}
}
}
Розбір коду:
ReadToEnd() читає весь файл у єдиний рядок (аналог File.ReadAllText()).ReadLine() читає один рядок (до символу \n або \r\n). Повертає null наприкінці файлу.Read() читає один символ (повертає int, потрібно привести до char).using System;
using System.IO;
class StreamWriterDemo
{
static void Main()
{
string logPath = "application.log";
// Запис з перезаписом
using (StreamWriter writer = new StreamWriter(logPath))
{
writer.WriteLine($"[{DateTime.Now}] Application started");
writer.WriteLine($"[{DateTime.Now}] Initializing components...");
writer.Write("Progress: ");
writer.Write("25%... ");
writer.WriteLine("50%... Done!");
}
// Додавання до існуючого файлу
using (StreamWriter writer = new StreamWriter(logPath, append: true))
{
writer.WriteLine($"[{DateTime.Now}] New log entry");
}
// Запис з кодуванням та буферизацією
using (StreamWriter writer = new StreamWriter(
path: logPath,
append: true,
encoding: System.Text.Encoding.UTF8,
bufferSize: 65536)) // 64 KB buffer
{
for (int i = 0; i < 1000; i++)
{
writer.WriteLine($"[{DateTime.Now}] Iteration {i}");
}
// writer.Flush(); // Примусово скинути буфер (викликається автоматично при Dispose)
}
// Виводимо результат
Console.WriteLine("=== ЛОГ ФАЙЛ ===");
Console.WriteLine(File.ReadAllText(logPath));
}
}
Розбір коду:
WriteLine() додає символ нового рядка (\n або \r\n залежно від ОС).Write() не додає новий рядок — дозволяє писати частинами.append: true додає текст у кінець файлу замість перезапису.StreamWriter буферизує дані для продуктивності.Flush() примусово записує буфер на диск. Dispose() викликає його автоматично.StreamWriter не записує кожен WriteLine() одразу на диск — він накопичує дані у буфері. Якщо програма аварійно завершиться до Dispose(), останні записи можуть втратитися. Використовуйте writer.AutoFlush = true для критичних логів.Для роботи з примітивними типами (int, float, bool) у бінарному форматі використовуйте BinaryReader та BinaryWriter. Вони автоматично обробляють конвертацію типів.
using System;
using System.IO;
// Модель даних
record PlayerData(string Name, int Level, float Health, bool IsOnline);
class BinaryDataDemo
{
static void Main()
{
string savePath = "player_save.dat";
// Створюємо дані гравця
var player = new PlayerData("Aragorn", 42, 95.5f, true);
// ЗАПИС у бінарний файл
using (FileStream fs = new FileStream(savePath, FileMode.Create))
using (BinaryWriter writer = new BinaryWriter(fs))
{
writer.Write(player.Name); // string
writer.Write(player.Level); // int (4 bytes)
writer.Write(player.Health); // float (4 bytes)
writer.Write(player.IsOnline);// bool (1 byte)
Console.WriteLine("Дані збережено!");
}
// ЧИТАННЯ з бінарного файлу
using (FileStream fs = new FileStream(savePath, FileMode.Open))
using (BinaryReader reader = new BinaryReader(fs))
{
string name = reader.ReadString();
int level = reader.ReadInt32();
float health = reader.ReadSingle();
bool isOnline = reader.ReadBoolean();
var loadedPlayer = new PlayerData(name, level, health, isOnline);
Console.WriteLine($"\nДані завантажено:");
Console.WriteLine($" Ім'я: {loadedPlayer.Name}");
Console.WriteLine($" Рівень: {loadedPlayer.Level}");
Console.WriteLine($" Здоров'я: {loadedPlayer.Health}");
Console.WriteLine($" Онлайн: {loadedPlayer.IsOnline}");
}
// Перевіряємо розмір файлу
FileInfo fileInfo = new FileInfo(savePath);
Console.WriteLine($"\nРозмір файлу: {fileInfo.Length} байт");
}
}
Розбір коду:
record для компактного визначення моделі даних.BinaryWriter.Write() має багато перевантажень для різних типів. Він автоматично конвертує їх у байти.ReadInt32(), ReadSingle(), ReadBoolean() — методи для конкретних типів.PlayerData (додасте поле), старі файли збережень не читатимуться. Для реальних проєктів потрібна система версійності форматів або використання JSON/XML/Protobuf.| Критерій | BinaryWriter | StreamWriter |
|---|---|---|
| Тип даних | Бінарні (int, float, byte) | Текст (string, char) |
| Розмір файлу | Компактний | Більший (текст) |
| Читабельність | Не читабельний людиною | Читабельний |
| Швидкість | Швидше | Повільніше |
| Використання | Протоколи, формати даних | Логи, конфігурації, CSV |
.NET надає вбудовані класи для стиснення та розпакування даних: GZipStream, DeflateStream та ZipArchive.
GZipStream використовує алгоритм GZIP (те саме, що HTTP compression).
using System;
using System.IO;
using System.IO.Compression;
class GZipDemo
{
static void Main()
{
string sourceFile = "large_document.txt";
string compressedFile = "large_document.txt.gz";
string decompressedFile = "large_document_restored.txt";
// Створюємо великий файл для демонстрації
File.WriteAllText(sourceFile, new string('A', 1_000_000)); // 1 MB букв 'A'
// СТИСНЕННЯ
Console.WriteLine("Стискання...");
using (FileStream sourceStream = new FileStream(sourceFile, FileMode.Open))
using (FileStream compressedStream = new FileStream(compressedFile, FileMode.Create))
using (GZipStream gzipStream = new GZipStream(compressedStream, CompressionMode.Compress))
{
sourceStream.CopyTo(gzipStream);
}
// РОЗПАКУВАННЯ
Console.WriteLine("Розпакування...");
using (FileStream compressedStream = new FileStream(compressedFile, FileMode.Open))
using (GZipStream gzipStream = new GZipStream(compressedStream, CompressionMode.Decompress))
using (FileStream decompressedStream = new FileStream(decompressedFile, FileMode.Create))
{
gzipStream.CopyTo(decompressedStream);
}
// Порівняння розмірів
long originalSize = new FileInfo(sourceFile).Length;
long compressedSize = new FileInfo(compressedFile).Length;
long restoredSize = new FileInfo(decompressedFile).Length;
Console.WriteLine($"\nРозміри:");
Console.WriteLine($" Оригінал: {originalSize:N0} байт");
Console.WriteLine($" Стиснутий: {compressedSize:N0} байт");
Console.WriteLine($" Відновлений: {restoredSize:N0} байт");
Console.WriteLine($" Коефіцієнт: {(double)compressedSize / originalSize:P2}");
// Перевірка коректності
string originalContent = File.ReadAllText(sourceFile);
string restoredContent = File.ReadAllText(decompressedFile);
Console.WriteLine($"\nДані ідентичні: {originalContent == restoredContent}");
}
}
Розбір коду:
GZipStream обгортає FileStream. Дані, записані у gzipStream, автоматично стискаються та записуються у compressedStream.CopyTo() копіює всі дані з одного потоку в інший.CompressionMode.Decompress.ZipArchive дозволяє створювати та читати ZIP-архіви з кількома файлами.
using System;
using System.IO;
using System.IO.Compression;
class ZipArchiveDemo
{
static void Main()
{
string archivePath = "backup.zip";
// Створюємо демонстраційні файли
Directory.CreateDirectory("TempData");
File.WriteAllText("TempData/file1.txt", "Вміст файлу 1");
File.WriteAllText("TempData/file2.txt", "Вміст файлу 2");
File.WriteAllText("TempData/file3.txt", "Вміст файлу 3");
// СТВОРЕННЯ ZIP-АРХІВУ
Console.WriteLine("Створення архіву...");
using (FileStream zipToOpen = new FileStream(archivePath, FileMode.Create))
using (ZipArchive archive = new ZipArchive(zipToOpen, ZipArchiveMode.Create))
{
// Додаємо файли
foreach (string filePath in Directory.GetFiles("TempData"))
{
string entryName = Path.GetFileName(filePath);
ZipArchiveEntry entry = archive.CreateEntry(entryName, CompressionLevel.Optimal);
using (Stream entryStream = entry.Open())
using (FileStream fileStream = new FileStream(filePath, FileMode.Open))
{
fileStream.CopyTo(entryStream);
}
Console.WriteLine($" Додано: {entryName}");
}
}
// ЧИТАННЯ ZIP-АРХІВУ
Console.WriteLine("\n=== ВМІСТ АРХІВУ ===");
using (FileStream zipToOpen = new FileStream(archivePath, FileMode.Open))
using (ZipArchive archive = new ZipArchive(zipToOpen, ZipArchiveMode.Read))
{
foreach (ZipArchiveEntry entry in archive.Entries)
{
Console.WriteLine($"📄 {entry.Name} ({entry.CompressedLength} / {entry.Length} байт)");
}
}
// РОЗПАКУВАННЯ
Console.WriteLine("\nРозпакування...");
ZipFile.ExtractToDirectory(archivePath, "ExtractedData", overwriteFiles: true);
Console.WriteLine("✅ Архів створено та розпаковано!");
}
}
Розбір коду:
ZipArchiveMode.Create — режим створення нового архіву.CreateEntry() додає елемент у архів. CompressionLevel.Optimal — баланс між швидкістю та ступенем стиснення.ZipArchiveMode.Read — режим читання.CompressedLength — розмір у архіві, Length — оригінальний розмір.ZipFile.ExtractToDirectory() — helper-метод для швидкого розпакування всього архіву.ZipFile — статичний клас з утилітарними методами (CreateFromDirectory, ExtractToDirectory). ZipArchive — для тонкого контролю (вибіркове читання, модифікація існуючих архівів).У сучасних додатках критично важливо не блокувати потоки під час I/O операцій. .NET підтримує async/await для всіх потокових операцій.
using System;
using System.IO;
using System.Threading.Tasks;
using System.Diagnostics;
class AsyncIODemo
{
static async Task Main()
{
string sourceFile = "large_video.mp4";
string destFile = "large_video_copy.mp4";
// Створюємо великий файл (100 MB) для демонстрації
Console.WriteLine("Створення демонстраційного файлу...");
await CreateLargeFileAsync(sourceFile, sizeInMB: 100);
// Синхронне копіювання
Console.WriteLine("\n[SYNC] Копіювання...");
Stopwatch sw = Stopwatch.StartNew();
File.Copy(sourceFile, destFile, overwrite: true);
sw.Stop();
Console.WriteLine($"Час: {sw.ElapsedMilliseconds} мс");
File.Delete(destFile);
// Асинхронне копіювання
Console.WriteLine("\n[ASYNC] Копіювання...");
sw.Restart();
await CopyFileAsync(sourceFile, destFile);
sw.Stop();
Console.WriteLine($"Час: {sw.ElapsedMilliseconds} мс");
Console.WriteLine("\n✅ Завершено!");
}
static async Task CopyFileAsync(string sourcePath, string destPath)
{
const int bufferSize = 81920; // 80 KB
using (FileStream sourceStream = new FileStream(
sourcePath,
FileMode.Open,
FileAccess.Read,
FileShare.Read,
bufferSize,
useAsync: true)) // Критично важливо!
using (FileStream destStream = new FileStream(
destPath,
FileMode.Create,
FileAccess.Write,
FileShare.None,
bufferSize,
useAsync: true))
{
await sourceStream.CopyToAsync(destStream);
}
}
static async Task CreateLargeFileAsync(string path, int sizeInMB)
{
byte[] buffer = new byte[1024 * 1024]; // 1 MB buffer
new Random().NextBytes(buffer); // Випадкові дані
using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, buffer.Length, useAsync: true))
{
for (int i = 0; i < sizeInMB; i++)
{
await fs.WriteAsync(buffer, 0, buffer.Length);
}
}
}
}
Розбір коду:
async Task Main() — підтримується з C# 7.1+.CopyToAsync() — асинхронна версія CopyTo().WriteAsync() — асинхронний запис. Не блокує потік під час запису на диск.useAsync: true. У такому разі операції все одно блокують потік! Завжди вказуйте useAsync: true у конструкторі FileStream для справжньої асинхронності.✅ Завжди використовуйте using
using statement гарантує виклик Dispose().🎯 Вибирайте правильний розмір буфера
⚡ Async для I/O bound операцій
🔒 Контролюйте FileShare
FileShare.Read для логів, які читають інші процеси.💾 Буферизація StreamWriter
AutoFlush = false (default) для продуктивності. AutoFlush = true тільки для критичних логів.🗜️ Стискайте великі файли
Створіть програму, яка:
StreamReader.[ERROR] та записує їх у окремий файл errors.log.Напишіть утиліту, яка:
backup_2026-01-10.zip.Створіть власний бінарний формат для збереження налаштувань додатку:
AppSettings з полями: string Theme, int FontSize, bool Notifications, string[] RecentFiles.SaveSettings(AppSettings, string path) за допомогою BinaryWriter.LoadSettings(string path) за допомогою BinaryReader.1).Ми розглянули потужну та гнучку архітектуру потоків у .NET:
useAsync: true