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
- 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-ой байт (нумерация с 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, если файл меньше!
}
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ