1. Вступ
Уявімо чайник із водою. Ви відкриваєте кран — вода починає текти. Можна набрати багато води й вилити все одразу, а можна наповнювати чайник поступово. Так само і з файлами: далеко не завжди зручно чи можливо відразу завантажити весь файл у памʼять. Файли бувають великими, а інколи джерелом даних може бути не файл, а, наприклад, мережеве з’єднання, де дані надходять поступово.
Якби ми завжди намагалися працювати лише з масивами байтів, то на великих файлах памʼять швидко закінчувалася б, та й для «нескінченних» потоків даних (наприклад, відео чи аудіо) цей підхід просто не спрацює. Саме тут виручає концепція потоку!
У .NET потік — це абстракція для послідовного доступу до даних: неважливо, що стоїть за джерелом — файл, мережа, памʼять чи навіть щось зовсім незвичне, на кшталт стисненого архіву. Потік дає змогу читати й записувати дані частинами — зазвичай блоками або байтами.
Головна ідея:
- Потік — канал передавання даних. Він схожий на конвеєр: ви можете записувати або зчитувати дані, не переймаючись тим, де і як вони зберігаються.
- Дані надходять послідовно: можна прочитати наступний блок лише після попереднього (або навпаки, якщо підтримується перемотування).
- У більшості випадків ви не тримаєте всі дані одночасно в памʼяті (компʼютер лише подякує).
Ця абстракція лежить в основі майже всіх операцій вводу-виводу у .NET: робота з файлами, мережами, архівами, навіть із консоллю!
2. Потоки System.IO.Stream
Наслідування й архітектура: System.IO.Stream
Майже всі потоки у .NET наслідуються від абстрактного класу System.IO.Stream. Він визначає основні методи для читання, запису, позиціонування в потоці та керування ним.
- Stream — базовий абстрактний клас
- FileStream — для роботи з файлами
- MemoryStream — для роботи з даними у памʼяті
- NetworkStream — для мережевої взаємодії
- CryptoStream — для шифрування/розшифрування
Коротке знайомство з ключовими властивостями й методами потоку
| Властивість / Метод | Опис |
|---|---|
|
Чи можна читати з цього потоку |
|
Чи можна писати у цей потік |
|
Чи можна переміщатися в межах потоку (підтримується не всюди) |
|
Довжина потоку (якщо підтримується; не для всіх потоків) |
|
Поточна позиція у потоці |
|
Читання даних |
|
Запис даних |
|
Переміщення в потоці |
|
Скидання буфера (запис усього накопиченого у потік) |
/ |
Закриття потоку та звільнення ресурсів |
Розберімося, як це виглядає «на практиці».
3. Приклад: читання й запис файлів через Stream
Ось мінімальний приклад, щоб побачити потік «у дії»:
// Відкриваємо файл для запису
using var stream = new FileStream("numbers.bin", FileMode.Create);
// Припустімо, що хочемо записати числа від 1 до 10 у файл
for (int i = 1; i <= 10; i++)
{
byte val = (byte)i;
stream.WriteByte(val); // Записуємо по одному байту
}
// Явно закриваємо файл, щоб відкрити його для читання
stream.Close();
// Тепер спробуємо прочитати ці числа назад
using var stream2 = new FileStream("numbers.bin", FileMode.Open);
int value;
while ((value = stream2.ReadByte()) != -1)
{
Console.WriteLine(value); // Виведе 1, 2, ... 10
}
Тут використано FileStream, який є справжнім потоком у повному сенсі цього слова: ви читаєте й записуєте дані блоками або по байтах.
Види потоків: де вони можуть траплятися?
Потік — це не обов’язково лише файл на диску. Ось кілька прикладів, де застосовується концепція потоку:
- Файл на диску (наприклад, FileStream — найчастіший випадок)
- Потік в оперативній памʼяті (MemoryStream — зручно для тимчасових або проміжних даних)
- Мережеве з’єднання (NetworkStream)
- Стиснення/архівація (GZipStream, DeflateStream)
- Шифрування (CryptoStream)
- Консольний ввід/вивід (так-так!) — технічно це також потоки
Це дає змогу писати код, не переймаючись конкретним джерелом чи приймачем даних: якщо ваш код працює з потоком, то він універсальний!
4. Корисні особливості
Читання й запис — це передавання даних частинами. Зазвичай через масиви байтів і методи Read, Write.
Приклад: читання файлу блоками
byte[] buffer = new byte[1024]; // Буфер на 1024 байти (1 КБ)
using var stream = new FileStream("bigfile.bin", FileMode.Open);
int bytesRead;
while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
{
// Обробляємо лише bytesRead байт всередині buffer
int sum = 0;
for (int i = 0; i < bytesRead; i++)
sum += buffer[i];
Console.WriteLine($"Сума блоку: {sum}");
}
Цей підхід застосовують повсюди — від антивірусів до музичних плеєрів.
Позиціонування у потоці (Position, Seek)
У більшості реалізацій потоків (наприклад, файлових) можна переміщатися в потоці — читати не просто наступний блок, а перейти до конкретної позиції й звідти працювати з даними.
using var stream = new FileStream("numbers.bin", FileMode.Open);
stream.Position = 5; // Переміщуємося на 6-й байт (нумерація з нуля)
int value = stream.ReadByte();
Console.WriteLine($"6-й байт у файлі: {value}");
Потоки бувають тільки для читання, тільки для запису або і те, і те
Деякі потоки підтримують лише один із варіантів:
- Файл, відкритий на запис: тільки Write()
- Потік для читання мережевих даних: тільки Read()
- У деяких незвичних випадках (наприклад, потік для виведення на принтер) взагалі неможливе перемотування чи позиціонування в потоці (не можна повернутися назад).
Перевіряйте підтримувані операції за допомогою властивостей CanRead, CanWrite, CanSeek:
using var stream = new FileStream("myfile.txt", FileMode.OpenOrCreate);
if (stream.CanRead)
Console.WriteLine("Читання підтримується");
if (stream.CanWrite)
Console.WriteLine("Запис підтримується");
if (stream.CanSeek)
Console.WriteLine("Можна переміщатися у файлі");
Буферизація у потоках
Майже всі потоки використовують внутрішні буфери для підвищення продуктивності. Буферизація зменшує кількість звернень до диска або мережі: дані накопичуються всередині, а потім передаються/записуються гуртом.
Метод Flush() дозволяє скинути буфер (наприклад, щоб гарантувати, що дані точно записані на диск):
using var stream = new FileStream("log.txt", FileMode.Append);
byte[] bytes = Encoding.UTF8.GetBytes("Hello, Stream!\n");
stream.Write(bytes, 0, bytes.Length);
stream.Flush(); // Гарантує, що дані точно записані на диск
Якщо ви записуєте критично важливі дані (наприклад, платіжні операції!), виклик Flush() — ваш друг.
5. Типові помилки під час роботи з потоками
Початківці часто припускаються таких помилок:
Забувають закрити потік — виникають витоки памʼяті, «завислі» файли тощо.
Плутають текстові й бінарні потоки — намагаються записати рядок байтовим методом, а потім отримують некоректні символи.
Використовують надто малий буфер (або взагалі не використовують буфер) — операції стають повільними.
Вважають, що Read() завжди читає рівно стільки байтів, скільки запитано; насправді метод може повернути менше, тому завжди перевіряйте повернене значення.
Не враховують, що не всі потоки підтримують позиціонування (Seek), особливо мережеві.
Наприклад:
// Поганий приклад: читання усіх байтів файлу без перевірки кількості фактично прочитаних байтів
byte[] buffer = new byte[1024];
using (var stream = new FileStream("data.bin", FileMode.Open))
{
int bytesRead = stream.Read(buffer, 0, 1024);
// bytesRead може бути меншим за 1024, якщо файл менший!
}
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ