JavaRush /Курсы /C# SELF /Класс FileStream

Класс FileStream

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

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, Append OpenOrCreate и др.)
  • 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.

2
Задача
C# SELF, 35 уровень, 3 лекция
Недоступна
Чтение текстового файла с использованием FileStream
Чтение текстового файла с использованием FileStream
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ