8.1.1. Основи роботи з файловою системою
8.1.1. Основи роботи з файловою системою
Вступ: Чому файлова система важлива?
Уявіть додаток, який не може зберігати дані. Налаштування втрачаються після кожного перезапуску, користувачі не можуть зберегти свою роботу, а логи помилок зникають безслідно. Звучить жахливо, чи не так? Саме тому робота з файловою системою (File System) є фундаментальною навичкою для будь-якого розробника.
File I/O (Input/Output) — це міст між вашим додатком та постійним сховищем даних. Це дозволяє:
- Зберігати конфігурації додатків
- Читати та записувати користувацькі дані
- Логувати події та помилки
- Обмінюватися даними між програмами
- Створювати резервні копії
- Обробляти великі набори даних
У цьому матеріалі ми розглянемо основи роботи з файловою системою в C#, від простих операцій до складних сценаріїв. Ви навчитеся не просто використовувати API, а розумітиму чому саме так, а не інакше.
Еволюція файлового I/O в .NET
Раніше розробники використовували низькорівневі бібліотеки та Win32 API для роботи з файлами — це було складно та схильно до помилок. З появою .NET Framework команда Microsoft створила потужний та інтуїтивний namespace System.IO, який абстрагує складність операційної системи.
Фундаментальні концепції файлової системи
Що таке файлова система?
Файлова система (File System) — це спосіб організації та зберігання файлів на носіях інформації (жорсткий диск, SSD, USB). Це як бібліотека: книги (файли) організовані по полицях (директоріях), а кожна книга має свою адресу (шлях).
Шляхи: Абсолютні vs Відносні
Абсолютний шлях (Absolute Path) — повна адреса файлу від кореня файлової системи:
Windows: C:\Users\John\Documents\report.txt
Linux: /home/john/documents/report.txt
macOS: /Users/john/Documents/report.txt
Відносний шлях (Relative Path) — адреса відносно поточної робочої директорії:
.\data\config.json // у підпапці data
..\shared\library.dll // на рівень вище, потім у shared
logs\app.log // у підпапці logs (без ./)
Path.Combine() замість конкатенації рядків для побудови шляхів. Це забезпечить крос-платформенність та правильну обробку роздільників (\ на Windows, / на Unix).Архітектура класів System.IO
Namespace System.IO містить два типи класів для роботи з файловою системою:
Коли використовувати що?
| Сценарій | Рекомендація | Причина |
|---|---|---|
| Одна операція з файлом | File або Directory | Менше overhead, простіший код |
| Багато операцій з одним файлом | FileInfo або DirectoryInfo | Кешування метаданих, краща продуктивність |
| Маніпуляції зі шляхами | Path | Крос-платформенні методи |
| Перевірка існування | File.Exists() / Directory.Exists() | Швидко, без створення об'єктів |
Клас File: Статичні операції з файлами
Клас File надає статичні методи для швидких операцій створення, копіювання, видалення, переміщення та читання/запису файлів.
Основні методи класу File
FileStream, який потрібно закрити!overwrite визначає, чи перезаписувати існуючий файл.Приклад 1: Читання та запис текстових файлів
Уявімо, що ми створюємо додаток для зберігання замінок користувача.
using System;
using System.IO;
class NotesApp
{
static void Main()
{
string filePath = "notes.txt";
// Перевіряємо, чи існує файл
if (!File.Exists(filePath))
{
Console.WriteLine("Файл нотаток не знайдено. Створюємо новий...");
File.WriteAllText(filePath, "Мої нотатки:\n");
}
// Додаємо нову нотатку
string newNote = $"[{DateTime.Now}] Вивчити File I/O в C#\n";
File.AppendAllText(filePath, newNote);
// Читаємо весь вміст
string allNotes = File.ReadAllText(filePath);
Console.WriteLine("=== ВСІ НОТАТКИ ===");
Console.WriteLine(allNotes);
}
}
Розбір коду:
- Рядок 8: Визначаємо відносний шлях до файлу. Файл буде створено у робочій директорії програми.
- Рядки 11-15: Перевіряємо існування файлу. Якщо файл не існує, створюємо його з заголовком за допомогою
File.WriteAllText(). - Рядок 18: Формуємо нотатку з поточною датою та часом.
- Рядок 19: Використовуємо
File.AppendAllText()для додавання нового рядка без перезапису існуючого вмісту. - Рядок 22: Зчитуємо весь файл у пам'ять за допомогою
File.ReadAllText().
ReadAllText() та ReadAllBytes() завантажують весь файл у пам'ять. Для файлів розміром понад 100 МБ використовуйте потокове читання (StreamReader або FileStream).Приклад 2: Робота з масивами рядків
Іноді потрібно обробляти файл порядково, наприклад, для аналізу логів.
using System;
using System.IO;
using System.Linq;
class LogAnalyzer
{
static void Main()
{
string logPath = "app.log";
// Створюємо демонстраційний лог
string[] sampleLogs =
{
"INFO: Application started",
"WARNING: Low memory",
"ERROR: Database connection failed",
"INFO: Retrying connection",
"INFO: Connection successful"
};
File.WriteAllLines(logPath, sampleLogs);
// Читаємо та аналізуємо
string[] logLines = File.ReadAllLines(logPath);
int errorCount = logLines.Count(line => line.Contains("ERROR"));
int warningCount = logLines.Count(line => line.Contains("WARNING"));
Console.WriteLine($"Всього записів: {logLines.Length}");
Console.WriteLine($"Помилки: {errorCount}");
Console.WriteLine($"Попередження: {warningCount}");
// Виводимо тільки помилки
Console.WriteLine("\n=== КРИТИЧНІ ПОМИЛКИ ===");
foreach (var line in logLines.Where(l => l.Contains("ERROR")))
{
Console.WriteLine(line);
}
}
}
Розбір коду:
- Рядки 12-19: Створюємо масив рядків для демонстрації.
- Рядок 20:
File.WriteAllLines()записує кожен елемент масиву як окремий рядок у файл. - Рядок 23:
File.ReadAllLines()повертає масив рядків, де кожен елемент — це рядок з файлу. - Рядки 25-26: Використовуємо LINQ для підрахунку записів з певними ключовими словами.
- Рядки 34-37: Фільтруємо та виводимо тільки рядки з помилками.
Приклад 3: Копіювання та переміщення файлів
using System;
using System.IO;
class FileManager
{
static void Main()
{
string sourceFile = "original.txt";
string backupFile = "backup/original_backup.txt";
string renamedFile = "renamed.txt";
// Створюємо оригінальний файл
File.WriteAllText(sourceFile, "Важливі дані!");
// Створюємо директорію для бекапу, якщо не існує
string backupDir = Path.GetDirectoryName(backupFile);
if (!Directory.Exists(backupDir))
{
Directory.CreateDirectory(backupDir);
}
// Копіюємо файл
File.Copy(sourceFile, backupFile, overwrite: true);
Console.WriteLine($"Створено резервну копію: {backupFile}");
// Переміщуємо (перейменовуємо) оригінал
File.Move(sourceFile, renamedFile, overwrite: true);
Console.WriteLine($"Файл перейменовано: {sourceFile} → {renamedFile}");
// Перевіряємо результат
Console.WriteLine($"\nІснує {sourceFile}? {File.Exists(sourceFile)}");
Console.WriteLine($"Існує {renamedFile}? {File.Exists(renamedFile)}");
Console.WriteLine($"Існує {backupFile}? {File.Exists(backupFile)}");
}
}
Розбір коду:
- Рядок 16:
Path.GetDirectoryName()витягує шлях директорії з повного шляху до файлу. - Рядок 19: Створюємо директорію, якщо її не існує (інакше
File.Copy()викине виняток). - Рядок 23:
File.Copy()створює копію файлу. Параметрoverwrite: trueдозволяє перезапис. - Рядок 27:
File.Move()переміщує файл (фактично перейменовує, якщо шлях у тій самій директорії).
File.Move() у .NET 5+ підтримує параметр overwrite. У старіших версіях потрібно спочатку видалити цільовий файл, якщо він існує, інакше виникне IOException.Клас Directory: Робота з директоріями
Клас Directory аналогічний до File, але працює з папками (директоріями).
Основні методи класу Directory
recursive: true видаляє всі вкладені файли та папки.Приклад 4: Створення та видалення директорій
using System;
using System.IO;
class DirectoryDemo
{
static void Main()
{
string projectDir = "MyProject";
string srcDir = Path.Combine(projectDir, "src");
string testsDir = Path.Combine(projectDir, "tests");
// Створюємо структуру проєкту
Console.WriteLine("Створення структури проєкту...");
Directory.CreateDirectory(srcDir);
Directory.CreateDirectory(testsDir);
// Створюємо файли
File.WriteAllText(Path.Combine(srcDir, "Program.cs"), "// Main code");
File.WriteAllText(Path.Combine(testsDir, "Tests.cs"), "// Unit tests");
Console.WriteLine($"Створено: {srcDir}");
Console.WriteLine($"Створено: {testsDir}");
// Виводимо вміст кореневої директорії
Console.WriteLine($"\n=== Вміст {projectDir} ===");
string[] subdirs = Directory.GetDirectories(projectDir);
foreach (var dir in subdirs)
{
Console.WriteLine($"📁 {Path.GetFileName(dir)}");
}
// Видалення (розкоментуйте для тесту)
// Directory.Delete(projectDir, recursive: true);
// Console.WriteLine($"\nПроєкт {projectDir} видалено.");
}
}
Розбір коду:
- Рядки 9-10: Використовуємо
Path.Combine()для побудови шляхів — це гарантує правильний роздільник на різних ОС. - Рядки 14-15:
Directory.CreateDirectory()створить всю ієрархію папок. ЯкщоprojectDirне існує, він буде створений автоматично. - Рядки 18-19: Створюємо файли всередині директорій.
- Рядок 26:
Directory.GetDirectories()повертає масив шляхів до всіх піддиректорій. - Рядок 29:
Path.GetFileName()витягує тільки назву директорії з повного шляху. - Рядок 33:
recursive: trueвидалить директорію з усім вмістом.
Directory.Delete(path, true)! Цей метод безповоротно видаляє всю директорію з усіма файлами та піддиректоріями. Немає "корзини" чи можливості відновлення. Використовуйте з обережністю!Приклад 5: Рекурсивний обхід директорій
Припустимо, ми хочемо знайти всі файли .cs у проєкті, включаючи вкладені папки.
using System;
using System.IO;
class DirectoryScanner
{
static void Main()
{
string rootPath = @"C:\Projects\MyApp"; // Змініть на ваш шлях
Console.WriteLine($"Пошук C# файлів у {rootPath}...\n");
try
{
ScanDirectory(rootPath, "*.cs");
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine($"Доступ заборонено: {ex.Message}");
}
}
static void ScanDirectory(string path, string searchPattern)
{
try
{
// Шукаємо файли в поточній директорії
string[] files = Directory.GetFiles(path, searchPattern);
foreach (var file in files)
{
Console.WriteLine($"📄 {file}");
}
// Рекурсивно обходимо піддиректорії
string[] subdirs = Directory.GetDirectories(path);
foreach (var dir in subdirs)
{
ScanDirectory(dir, searchPattern); // Рекурсія!
}
}
catch (UnauthorizedAccessException)
{
Console.WriteLine($"⛔ Немає доступу до: {path}");
}
}
}
Розбір коду:
- Рядок 27:
Directory.GetFiles(path, pattern)шукає файли з певним шаблоном (*.cs— всі файли з розширенням.cs). - Рядки 34-38: Рекурсивний обхід: для кожної піддиректорії викликаємо
ScanDirectory()знову. - Рядки 40-43: Обробка
UnauthorizedAccessExceptionдля системних папок (наприклад,System Volume Informationна Windows).
Directory.EnumerateFiles() замість GetFiles(). Це ледаче перерахування дозволяє обробляти файли по одному без завантаження всього списку в пам'ять.Клас Path: Маніпуляції зі шляхами
Path — це утилітарний клас, який НЕ працює з реальною файловою системою, а лише виконує маніпуляції з рядками шляхів. Це критично важливо для крос-платформенних додатків.
Основні методи класу Path
.txt).Приклад 6: Розбір шляхів
using System;
using System.IO;
class PathDemo
{
static void Main()
{
string filePath = @"C:\Users\John\Documents\report.docx";
Console.WriteLine("=== АНАЛІЗ ШЛЯХУ ===");
Console.WriteLine($"Повний шлях: {filePath}");
Console.WriteLine($"Директорія: {Path.GetDirectoryName(filePath)}");
Console.WriteLine($"Ім'я файлу: {Path.GetFileName(filePath)}");
Console.WriteLine($"Без розширення: {Path.GetFileNameWithoutExtension(filePath)}");
Console.WriteLine($"Розширення: {Path.GetExtension(filePath)}");
// Зміна розширення
string pdfPath = Path.ChangeExtension(filePath, ".pdf");
Console.WriteLine($"\nЗмінено на PDF: {pdfPath}");
// Побудова шляхів
string projectRoot = @"C:\Projects\MyApp";
string configPath = Path.Combine(projectRoot, "config", "appsettings.json");
Console.WriteLine($"\nПобудований шлях: {configPath}");
// Системні шляхи
Console.WriteLine($"\nТимчасова папка: {Path.GetTempPath()}");
Console.WriteLine($"Роздільник: '{Path.DirectorySeparatorChar}'");
}
}
Вивід на Windows:
=== АНАЛІЗ ШЛЯХУ ===
Повний шлях: C:\Users\John\Documents\report.docx
Директорія: C:\Users\John\Documents
Ім'я файлу: report.docx
Без розширення: report
Розширення: .docx
Змінено на PDF: C:\Users\John\Documents\report.pdf
Побудований шлях: C:\Projects\MyApp\config\appsettings.json
Тимчасова папка: C:\Users\John\AppData\Local\Temp\
Роздільник: '\'
Розбір коду:
- Рядок 12-15: Використовуємо різні методи для витягування частин шляху.
- Рядок 18:
Path.ChangeExtension()замінює існуюче розширення на нове. - Рядок 23:
Path.Combine()автоматично додає правильні роздільники (\на Windows,/на Linux). - Рядок 27-28: Демонстрація системних шляхів та роздільників.
/ замість \), але код залишається однаковим. Це магія Path.Combine()!FileInfo та DirectoryInfo: Об'єктно-орієнтований підхід
Коли потрібно виконати кілька операцій з одним файлом або директорією, краще використовувати FileInfo та DirectoryInfo. Ці класи кешують метадані (розмір, дату створення, атрибути) та надають зручні властивості.
FileInfo: Властивості та методи
using System;
using System.IO;
class FileInfoDemo
{
static void Main()
{
string filePath = "sample.txt";
// Створюємо файл для демонстрації
File.WriteAllText(filePath, "Hello, FileInfo!");
// Створюємо об'єкт FileInfo
FileInfo fileInfo = new FileInfo(filePath);
// Властивості
Console.WriteLine("=== ІНФОРМАЦІЯ ПРО ФАЙЛ ===");
Console.WriteLine($"Повний шлях: {fileInfo.FullName}");
Console.WriteLine($"Ім'я: {fileInfo.Name}");
Console.WriteLine($"Розмір: {fileInfo.Length} байт");
Console.WriteLine($"Створено: {fileInfo.CreationTime}");
Console.WriteLine($"Змінено: {fileInfo.LastWriteTime}");
Console.WriteLine($"Тільки читання: {fileInfo.IsReadOnly}");
Console.WriteLine($"Існує: {fileInfo.Exists}");
// Методи
FileInfo backup = fileInfo.CopyTo("sample_backup.txt", overwrite: true);
Console.WriteLine($"\nСтворено копію: {backup.Name}");
fileInfo.MoveTo("renamed_sample.txt", overwrite: true);
Console.WriteLine($"Перейменовано на: {fileInfo.Name}");
// Після переміщення Exists поверне false для старого шляху
fileInfo.Refresh(); // Оновлюємо кеш метаданих
Console.WriteLine($"Існує (старий): {new FileInfo(filePath).Exists}");
Console.WriteLine($"Існує (новий): {fileInfo.Exists}");
}
}
Розбір коду:
- Рядок 14: Створюємо об'єкт
FileInfo. Це НЕ створює файл, а лише зчитує його метадані. - Рядки 18-24: Властивості
FileInfoнадають інформацію про файл (розмір, дати, атрибути). - Рядок 27:
CopyTo()повертає новий об'єктFileInfoдля копії. - Рядок 30:
MoveTo()переміщує файл. Важливо: об'єктfileInfoтепер вказує на новий шлях! - Рядок 34:
Refresh()оновлює кеш метаданих. Без цьогоExistsможе повернути застарілу інформацію.
FileInfo буде ефективнішим за статичні методи File.DirectoryInfo: Робота з директоріями
using System;
using System.IO;
using System.Linq;
class DirectoryInfoDemo
{
static void Main()
{
string dirPath = "ProjectFolder";
// Створюємо директорію
DirectoryInfo dirInfo = new DirectoryInfo(dirPath);
if (!dirInfo.Exists)
{
dirInfo.Create();
Console.WriteLine($"Створено: {dirInfo.FullName}");
}
// Створюємо піддиректорії та файли
dirInfo.CreateSubdirectory("src");
dirInfo.CreateSubdirectory("docs");
File.WriteAllText(Path.Combine(dirPath, "README.md"), "# Project");
// Отримуємо всі файли та директорії
Console.WriteLine("\n=== ВМІСТ ===");
FileInfo[] files = dirInfo.GetFiles();
DirectoryInfo[] subdirs = dirInfo.GetDirectories();
foreach (var dir in subdirs)
{
Console.WriteLine($"📁 {dir.Name} (створено {dir.CreationTime:yyyy-MM-dd})");
}
foreach (var file in files)
{
Console.WriteLine($"📄 {file.Name} ({file.Length} байт)");
}
// Рекурсивний пошук
Console.WriteLine("\n=== ВСІ ФАЙЛИ (РЕКУРСИВНО) ===");
FileInfo[] allFiles = dirInfo.GetFiles("*", SearchOption.AllDirectories);
foreach (var file in allFiles)
{
Console.WriteLine($" {file.FullName}");
}
// Статистика
long totalSize = allFiles.Sum(f => f.Length);
Console.WriteLine($"\nВсього файлів: {allFiles.Length}");
Console.WriteLine($"Загальний розмір: {totalSize} байт");
}
}
Розбір коду:
- Рядки 12-16:
DirectoryInfoможна створити для неіснуючої директорії. ПеревіряємоExistsта викликаємоCreate(). - Рядок 20:
CreateSubdirectory()створює піддиректорію відносно поточної. - Рядки 26-27:
GetFiles()таGetDirectories()повертають масивиFileInfoтаDirectoryInfo. - Рядок 41:
SearchOption.AllDirectoriesвмикає рекурсивний пошук (включно з вкладеними папками). - Рядок 47: Використовуємо LINQ для підрахунку загального розміру всіх файлів.
Порівняльна таблиця: File vs FileInfo, Directory vs DirectoryInfo
| Критерій | File / Directory | FileInfo / DirectoryInfo |
|---|---|---|
| Коли використовувати | Одна операція | Кілька операцій з одним файлом/директорією |
| Продуктивність | Менше overhead | Кращe для повторних операцій (кешування) |
| Синтаксис | Статичні методи | Об'єктно-орієнтований підхід |
| Метадані | Кожен виклик — новий запит | Кешуються (потрібен Refresh()) |
| Приклад використання | File.Exists(path) | new FileInfo(path).Exists |
| Створення об'єкту | Не потрібно | Необхідно створити екземпляр |
Практичні завдання
Завдання 1: Менеджер конфігурацій
Створіть програму, яка:
- Перевіряє наявність файлу
config.jsonу директоріїsettings. - Якщо файл не існує, створює його з дефолтним вмістом:
{"theme": "dark", "language": "uk"}. - Зчитує вміст та виводить у консоль.
- Створює резервну копію у
settings/backup/.
Завдання 2: Пошук дублікатів
Напишіть програму, яка знаходить файли з однаковими іменами (але в різних директоріях) всередині заданої папки. Використайте DirectoryInfo та рекурсивний пошук.
Завдання 3: Очищення логів
Створіть утиліту, яка видаляє всі .log файли старше 7 днів у вказаній директорії. Використайте FileInfo.LastWriteTime для перевірки дати.
Резюме: Що ми вивчили?
Клас File
Клас Directory
Клас Path
FileInfo та DirectoryInfo
Ключові поняття
- Абсолютний vs Відносний шлях: Розумійте різницю та завжди використовуйте
Path.Combine(). - Статичні vs Об'єктні методи: Вибирайте підхід залежно від кількості операцій.
- Безпека: Завжди обробляйте
UnauthorizedAccessExceptionтаIOException. - Продуктивність: Для великих файлів використовуйте потокове читання (про це в наступному розділі).