1. Класс FileStream: работа по кусочкам
FileStream можно представить, как водопроводную трубу, напрямую подключенную к вашему файлу на диске. Через эту трубу вы можете контролируемо пропускать данные: отправлять байты в файл (запись) или получать байты из файла (чтение). В отличие от более высокоуровневых методов, которые просто "дают вам стакан воды", FileStream дает вам прямой доступ к "крану", позволяя регулировать поток воды (байтов) по мере необходимости.
FileStream работает на уровне байтов. Это означает, что он не заботится о том, текст это или изображение; для него всё — просто последовательность байтов. Если вы работаете с текстовыми файлами через FileStream, вам нужно будет самостоятельно преобразовать строки в байты (используя кодировку, например, UTF-8) при записи и обратно при чтении.
Когда именно нужен FileStream?
- Работа с очень большими файлами: Когда файл настолько огромен, что его нельзя или нецелесообразно загружать целиком в оперативную память (например, гигабайтные логи, видеофайлы). FileStream позволяет читать или записывать данные порциями (чанками), что использует память эффективнее.
- Бинарные данные: Если вы работаете с файлами, которые не являются обычным текстом (изображения, аудио, видео, сериализованные объекты, файлы баз данных), FileStream — это основной инструмент, так как он предоставляет прямой доступ к байтам.
- Детальный контроль над режимами доступа: Вам нужно открыть файл для чтения *и* записи одновременно? Или открыть его так, чтобы другие программы не могли к нему получить доступ? Или наоборот, чтобы несколько процессов могли читать файл одновременно? FileStream предоставляет полный контроль над этими режимами.
- Асинхронные операции: В современных высокопроизводительных приложениях (например, веб-серверах) критически важно, чтобы файловые операции не блокировали основной поток выполнения. FileStream поддерживает асинхронные методы (ReadAsync, WriteAsync), позволяя программе оставаться отзывчивой.
- Частичное чтение/запись или произвольный доступ: Если вам нужно прочитать данные из определенного места в файле (например, с 500-го байта) или записать данные в середину файла, FileStream позволяет управлять позицией файлового указателя.
Типичная ошибка новичков: Использовать FileStream для самых простых задач, таких как запись одной строки текста в небольшой файл. В таких случаях он будет избыточен. FileStream — это инструмент для специфических и более сложных сценариев.
2. Создание и открытие FileStream
Для того чтобы начать работу с файлом через FileStream, вам необходимо создать его экземпляр. Это делается с помощью конструктора, который принимает несколько важных параметров, определяющих, как вы хотите взаимодействовать с файлом.
using System;
using System.IO; // Обязательно для работы с FileStream
using System.Text; // Для работы с кодировками (преобразования строк в байты и наоборот)
Пример базового чтения текстового файла с помощью FileStream:
Сначала создадим файл, чтобы было что читать.
// Создадим простой текстовый файл для демонстрации
string path = "example_filestream.txt";
// Открываем поток для чтения.
FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read);
// Создаем буфер (массив байтов) для временного хранения прочитанных данных.
byte[] buffer = new byte[fs.Length];
// Считываем байты из потока в буфер.
int bytesRead = fs.Read(buffer, 0, buffer.Length);
// Преобразуем прочитанные байты обратно в строку, используя нужную кодировку (например, UTF-8).
string content = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine("Прочитано из файла через FileStream: " + content);
fs.Close(); //закрываем поток
Основные параметры
- FileMode: как именно открывать файл (
Open,Create,AppendOpenOrCreateи др.) - FileAccess: что делать с файлом (
Read,Write,ReadWrite) - FileShare: можно ли другим процессам одновременно использовать этот файл (обычно для простых задач не нужен)
Типичная ошибка: Если вы забыли закрыть FileStream, файл может "залочиться" — не получится его потом удалить или открыть заново!
3. Параметры конструктора FileStream
Конструктор FileStream позволяет очень точно настроить, как именно вы хотите открыть файл. Вот его основные параметры:
new FileStream(
string path, // Путь к файлу (абсолютный или относительный)
FileMode mode, // Как открывать файл (создать, открыть, перезаписать и т.д.)
FileAccess access, // Какой доступ разрешен (только чтение, только запись, чтение+запись)
FileShare share // Как другие процессы могут обращаться к файлу, пока он открыт (опционально)
);
Давайте разберем значения перечислений FileMode и FileAccess:
FileMode (Режим открытия файла):
Определяет, как операционная система должна обращаться с файлом при открытии.
FileMode.Open: Открывает существующий файл. Если файл по указанному пути не найден, выбрасывается FileNotFoundException.FileMode.Create: Создает новый файл. Если файл с таким именем уже существует, он будет полностью перезаписан (его содержимое будет удалено).
FileAccess (Права доступа к файлу):
Определяет, какие операции разрешены вашей программе с открытым файлом.
FileAccess.Read: Файл открыт только для чтения. Вы не сможете записывать в него данные.FileAccess.Write: Файл открыт только для записи. Вы не сможете читать из него данные.FileAccess.ReadWrite: Файл открыт как для чтения, так и для записи. Это наиболее гибкий, но и потенциально более сложный режим, так как вам нужно управлять позицией в потоке.
FileShare (Совместный доступ):
Определяет, как другие процессы могут открывать тот же файл, пока он открыт вашей программой. Очень важно для предотвращения блокировок.
FileShare.Read: Другие процессы могут читать файл, пока он открыт вашим FileStream, но не могут записывать.FileShare.Write: Другие процессы могут записывать в файл, пока он открыт вашим FileStream, но не могут читать.FileShare.ReadWrite: Другие процессы могут читать и записывать в файл. Это самый либеральный режим, но может привести к конфликтам, если несколько программ одновременно модифицируют файл.
Подробнее про режимы работы в следующей лекции.
4. Чтение и запись данных через FileStream
Работая с FileStream, вы манипулируете байтами. Это означает, что для текстовых данных вам нужно выполнять преобразования между строками и массивами байтов, используя классы кодировок (например, System.Text.Encoding.UTF8).
Запись данных в файл с FileStream
При записи вы преобразуете свои данные (например, строку) в массив байтов, а затем записываете эти байты в поток.
string outputPath = "user_data.txt";
string userName = "Иван Петров";
// Преобразуем строку в массив байтов, используя кодировку UTF-8
byte[] userNameBytes = Encoding.UTF8.GetBytes(userName);
// Открываем FileStream для записи.
FileStream fsWrite = new FileStream(outputPath, FileMode.Create, FileAccess.Write)ж
// Записываем массив байтов в поток.
fsWrite.Write(userNameBytes, 0, userNameBytes.Length);
Console.WriteLine($"Имя '{userName}' успешно записано в файл '{outputPath}'.");
fsWrite.Close(); //закрываем поток
Чтение данных из файла с FileStream
При чтении вы считываете байты из потока в буфер, а затем преобразуете эти байты обратно в нужный вам формат (например, строку).
string inputPath = "user_data.txt";
// Открываем FileStream для чтения
FileStream fsRead = new FileStream(inputPath, FileMode.Open, FileAccess.Read);
// Создаем буфер размером с файл для считывания всех байтов.
byte[] buffer = new byte[fsRead.Length];
// Считываем байты из потока в буфер.
int bytesRead = fsRead.Read(buffer, 0, buffer.Length);
// Преобразуем прочитанные байты в строку.
string loadedUserName = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine($"Имя из файла (через FileStream): {loadedUserName}");
fsRead.Close(); //закрываем поток
5. Работа с большими файлами
Главное преимущество FileStream перед методами File.ReadAll... — это возможность читать и писать файл по частям, что критически важно для работы с большими файлами, которые не помещаются целиком в оперативную память.
Чтение больших файлов по частям
Когда вы читаете файл по частям, вы используете буфер (массив байтов) фиксированного размера и считываете данные в него до тех пор, пока не достигнете конца файла.
string bigFilePath = "bigfile.bin"; //большой файл
int bufferSize = 4096; // Размер буфера, например, 4 КБ
byte[] buffer = new byte[bufferSize]; // Создаем буфер
int bytesRead; // Количество фактически прочитанных байтов
long totalBytesRead = 0; // Общее количество прочитанных байтов
// открываем поток
FileStream fs = new FileStream(bigFilePath, FileMode.Open, FileAccess.Read);
int chunkNumber = 1;
// Цикл продолжается, пока fs.Read() возвращает положительное число байтов
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
totalBytesRead += bytesRead;
Console.WriteLine($"Часть {chunkNumber++}: считано {bytesRead} байт. Всего: {totalBytesRead} байт.");
// !!! Здесь происходит обработка прочитанных "кусочков" данных !!!
}
fs.Close(); //закрываем поток
Важная заметка: Метод fs.Read(buffer, offset, count) не гарантирует, что он заполнит весь buffer. Он возвращает количество фактически прочитанных байтов, которое может быть меньше count (особенно в конце файла). Всегда используйте возвращаемое значение bytesRead для корректной обработки данных.
Запись больших данных по частям
Аналогично чтению, вы можете записывать большие объемы данных в файл по частям.
string bigOutputPath = "big_output.txt";
string longText = new string('A', 1_000_000); // Строка из миллиона символов 'A'
byte[] longTextBytes = Encoding.UTF8.GetBytes(longText);
int writeBufferSize = 1024; // Записываем по 1 КБ за раз
// FileMode.Create: создаем файл
FileStream fsWrite = new FileStream(bigOutputPath, FileMode.Create, FileAccess.Write);
for (int i = 0; i < longTextBytes.Length; i += writeBufferSize)
{
// Вычисляем, сколько байтов осталось записать в текущей итерации
int bytesToWrite = Math.Min(writeBufferSize, longTextBytes.Length - i);
// Записываем порцию данных из longTextBytes, начиная со смещения 'i'
fsWrite.Write(longTextBytes, i, bytesToWrite);
Console.WriteLine($"Записано {bytesToWrite} байт");
}
fsWrite.Close(); //закрываем поток
6. Неочевидные подводные камни и типичные ошибки
Даже с таким мощным инструментом, как FileStream, есть нюансы, о которых стоит помнить:
Буферизация и Flush(): Как уже упоминалось, данные могут оставаться в буфере в памяти, не достигая диска. Если вы не используете using и не вызываете Close()/Dispose(), а просто завершаете программу, данные могут быть потеряны. Всегда вызывайте Flush() (или полагайтесь на using/Close()) для гарантии записи данных.
Обработка исключений: Ошибки ввода/вывода (например, IOException при попытке записать на заполненный диск, UnauthorizedAccessException из-за недостатка прав, FileNotFoundException при попытке открыть несуществующий файл в режиме Open) — это обычное дело. Всегда оборачивайте операции с FileStream в try-catch блоки.
Кодировки: При работе с текстовыми данными через FileStream (который работает с байтами), вы несете полную ответственность за выбор правильной кодировки (Encoding.UTF8, Encoding.ASCII, Encoding.Unicode и т.д.) при преобразовании строк в байты и обратно. Неправильный выбор кодировки приведет к "кракозябрам".
Управление позицией: Если вы используете Seek(), будьте внимательны с параметрами offset и origin, чтобы не переместиться за пределы файла или в неожиданное место.
Файловые блокировки (FileShare): Если вы открываете файл с FileShare.None, другие процессы не смогут получить к нему доступ. Это может быть проблемой, если другая программа (или даже другой поток в вашей программе) попытается использовать тот же файл. Всегда выбирайте наиболее подходящий режим FileShare.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ