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

Потоки ввода-вывода: Stream

C# SELF
36 уровень , 1 лекция
Открыта

1. Введение

Давайте представим чайник с водой. Вы открываете кран — вода начинает течь. Можно набрать много воды и налить все разом, а можно наполнить чайник по чуть-чуть. Точно так же и с файлами — далеко не всегда удобно или возможно сразу загрузить весь файл в память. Файлы бывают большими, а иногда источником данных может быть не файл, а, например, сетевое соединение, где данные поступают постепенно.

Если бы мы всегда пытались работать просто с массивами байтов, то на больших файлах у нас быстро закончилась бы память, да и для "бесконечных" потоков данных (например, видео или аудиопотоков) этот подход просто не сработает. Вот тут и приходит на помощь концепция потока!

В .NET поток — это абстракция для последовательного доступа к данным: неважно, что стоит за источником — файл, сеть, память, или даже что-то совсем экзотическое вроде сжатого архива. Поток позволяет вам читать и писать данные по частям, обычно блоками или байтами.

Основная идея:

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

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

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

Наследование и архитектура: System.IO.Stream

Почти все потоки в .NET наследуются от абстрактного класса System.IO.Stream. Он определяет основные методы для чтения, записи, перемещения по потоку и управления им.

     classDiagram
     class Stream {
     +Read()
     +Write()
     +Seek()
     +CanRead
     +CanWrite
     +CanSeek
     +Length
     +Position
     }
     class FileStream
     class MemoryStream
     class NetworkStream
     class CryptoStream
     Stream <|-- FileStream
     Stream <|-- MemoryStream
     Stream <|-- NetworkStream
     Stream <|-- CryptoStream
    
Схема наследования потоков в .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-ой байт (нумерация с 0)
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, если файл меньше!
}
2
Задача
C# SELF, 36 уровень, 1 лекция
Недоступна
Проверка свойств стрима
Проверка свойств стрима
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ