JavaRush /Курси /C# SELF /Потоки вводу-виводу: Strea...

Потоки вводу-виводу: Stream

C# SELF
Рівень 36 , Лекція 1
Відкрита

1. Вступ

Уявімо чайник із водою. Ви відкриваєте кран — вода починає текти. Можна набрати багато води й вилити все одразу, а можна наповнювати чайник поступово. Так само і з файлами: далеко не завжди зручно чи можливо відразу завантажити весь файл у памʼять. Файли бувають великими, а інколи джерелом даних може бути не файл, а, наприклад, мережеве з’єднання, де дані надходять поступово.

Якби ми завжди намагалися працювати лише з масивами байтів, то на великих файлах памʼять швидко закінчувалася б, та й для «нескінченних» потоків даних (наприклад, відео чи аудіо) цей підхід просто не спрацює. Саме тут виручає концепція потоку!

У .NET потік — це абстракція для послідовного доступу до даних: неважливо, що стоїть за джерелом — файл, мережа, памʼять чи навіть щось зовсім незвичне, на кшталт стисненого архіву. Потік дає змогу читати й записувати дані частинами — зазвичай блоками або байтами.

Головна ідея:

  • Потік — канал передавання даних. Він схожий на конвеєр: ви можете записувати або зчитувати дані, не переймаючись тим, де і як вони зберігаються.
  • Дані надходять послідовно: можна прочитати наступний блок лише після попереднього (або навпаки, якщо підтримується перемотування).
  • У більшості випадків ви не тримаєте всі дані одночасно в памʼяті (компʼютер лише подякує).

Ця абстракція лежить в основі майже всіх операцій вводу-виводу у .NET: робота з файлами, мережами, архівами, навіть із консоллю!

2. Потоки System.IO.Stream

Наслідування й архітектура: System.IO.Stream

Майже всі потоки у .NET наслідуються від абстрактного класу System.IO.Stream. Він визначає основні методи для читання, запису, позиціонування в потоці та керування ним.

Схема наслідування потоків у .NET
  • Stream — базовий абстрактний клас
  • FileStream — для роботи з файлами
  • MemoryStream — для роботи з даними у памʼяті
  • NetworkStream — для мережевої взаємодії
  • CryptoStream — для шифрування/розшифрування

Коротке знайомство з ключовими властивостями й методами потоку

Властивість / Метод Опис
CanRead
Чи можна читати з цього потоку
CanWrite
Чи можна писати у цей потік
CanSeek
Чи можна переміщатися в межах потоку (підтримується не всюди)
Length
Довжина потоку (якщо підтримується; не для всіх потоків)
Position
Поточна позиція у потоці
Read(...)
Читання даних
Write(...)
Запис даних
Seek(...)
Переміщення в потоці
Flush()
Скидання буфера (запис усього накопиченого у потік)
Close()
/
Dispose()
Закриття потоку та звільнення ресурсів

Розберімося, як це виглядає «на практиці».

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, якщо файл менший!
}
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ