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.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ