Уявіть додаток, який не може зберігати дані. Налаштування втрачаються після кожного перезапуску, користувачі не можуть зберегти свою роботу, а логи помилок зникають безслідно. Звучить жахливо, чи не так? Саме тому робота з файловою системою (File System) є фундаментальною навичкою для будь-якого розробника.
File I/O (Input/Output) — це міст між вашим додатком та постійним сховищем даних. Це дозволяє:
У цьому матеріалі ми розглянемо основи роботи з файловою системою в C#, від простих операцій до складних сценаріїв. Ви навчитеся не просто використовувати API, а розумітиму чому саме так, а не інакше.
Раніше розробники використовували низькорівневі бібліотеки та Win32 API для роботи з файлами — це було складно та схильно до помилок. З появою .NET Framework команда Microsoft створила потужний та інтуїтивний namespace System.IO, який абстрагує складність операційної системи.
Файлова система (File System) — це спосіб організації та зберігання файлів на носіях інформації (жорсткий диск, SSD, USB). Це як бібліотека: книги (файли) організовані по полицях (директоріях), а кожна книга має свою адресу (шлях).
Абсолютний шлях (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).Namespace System.IO містить два типи класів для роботи з файловою системою:
Коли використовувати що?
| Сценарій | Рекомендація | Причина |
|---|---|---|
| Одна операція з файлом | File або Directory | Менше overhead, простіший код |
| Багато операцій з одним файлом | FileInfo або DirectoryInfo | Кешування метаданих, краща продуктивність |
| Маніпуляції зі шляхами | Path | Крос-платформенні методи |
| Перевірка існування | File.Exists() / Directory.Exists() | Швидко, без створення об'єктів |
Клас File надає статичні методи для швидких операцій створення, копіювання, видалення, переміщення та читання/запису файлів.
FileStream, який потрібно закрити!overwrite визначає, чи перезаписувати існуючий файл.Уявімо, що ми створюємо додаток для зберігання замінок користувача.
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);
}
}
Розбір коду:
File.WriteAllText().File.AppendAllText() для додавання нового рядка без перезапису існуючого вмісту.File.ReadAllText().ReadAllText() та ReadAllBytes() завантажують весь файл у пам'ять. Для файлів розміром понад 100 МБ використовуйте потокове читання (StreamReader або FileStream).Іноді потрібно обробляти файл порядково, наприклад, для аналізу логів.
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);
}
}
}
Розбір коду:
File.WriteAllLines() записує кожен елемент масиву як окремий рядок у файл.File.ReadAllLines() повертає масив рядків, де кожен елемент — це рядок з файлу.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)}");
}
}
Розбір коду:
Path.GetDirectoryName() витягує шлях директорії з повного шляху до файлу.File.Copy() викине виняток).File.Copy() створює копію файлу. Параметр overwrite: true дозволяє перезапис.File.Move() переміщує файл (фактично перейменовує, якщо шлях у тій самій директорії).File.Move() у .NET 5+ підтримує параметр overwrite. У старіших версіях потрібно спочатку видалити цільовий файл, якщо він існує, інакше виникне IOException.Клас Directory аналогічний до File, але працює з папками (директоріями).
recursive: true видаляє всі вкладені файли та папки.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} видалено.");
}
}
Розбір коду:
Path.Combine() для побудови шляхів — це гарантує правильний роздільник на різних ОС.Directory.CreateDirectory() створить всю ієрархію папок. Якщо projectDir не існує, він буде створений автоматично.Directory.GetDirectories() повертає масив шляхів до всіх піддиректорій.Path.GetFileName() витягує тільки назву директорії з повного шляху.recursive: true видалить директорію з усім вмістом.Directory.Delete(path, true)! Цей метод безповоротно видаляє всю директорію з усіма файлами та піддиректоріями. Немає "корзини" чи можливості відновлення. Використовуйте з обережністю!Припустимо, ми хочемо знайти всі файли .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}");
}
}
}
Розбір коду:
Directory.GetFiles(path, pattern) шукає файли з певним шаблоном (*.cs — всі файли з розширенням .cs).ScanDirectory() знову.UnauthorizedAccessException для системних папок (наприклад, System Volume Information на Windows).Directory.EnumerateFiles() замість GetFiles(). Це ледаче перерахування дозволяє обробляти файли по одному без завантаження всього списку в пам'ять.Path — це утилітарний клас, який НЕ працює з реальною файловою системою, а лише виконує маніпуляції з рядками шляхів. Це критично важливо для крос-платформенних додатків.
.txt).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\
Роздільник: '\'
Розбір коду:
Path.ChangeExtension() замінює існуюче розширення на нове.Path.Combine() автоматично додає правильні роздільники (\ на Windows, / на Linux)./ замість \), але код залишається однаковим. Це магія Path.Combine()!Коли потрібно виконати кілька операцій з одним файлом або директорією, краще використовувати FileInfo та DirectoryInfo. Ці класи кешують метадані (розмір, дату створення, атрибути) та надають зручні властивості.
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}");
}
}
Розбір коду:
FileInfo. Це НЕ створює файл, а лише зчитує його метадані.FileInfo надають інформацію про файл (розмір, дати, атрибути).CopyTo() повертає новий об'єкт FileInfo для копії.MoveTo() переміщує файл. Важливо: об'єкт fileInfo тепер вказує на новий шлях!Refresh() оновлює кеш метаданих. Без цього Exists може повернути застарілу інформацію.FileInfo буде ефективнішим за статичні методи File.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} байт");
}
}
Розбір коду:
DirectoryInfo можна створити для неіснуючої директорії. Перевіряємо Exists та викликаємо Create().CreateSubdirectory() створює піддиректорію відносно поточної.GetFiles() та GetDirectories() повертають масиви FileInfo та DirectoryInfo.SearchOption.AllDirectories вмикає рекурсивний пошук (включно з вкладеними папками).| Критерій | File / Directory | FileInfo / DirectoryInfo |
|---|---|---|
| Коли використовувати | Одна операція | Кілька операцій з одним файлом/директорією |
| Продуктивність | Менше overhead | Кращe для повторних операцій (кешування) |
| Синтаксис | Статичні методи | Об'єктно-орієнтований підхід |
| Метадані | Кожен виклик — новий запит | Кешуються (потрібен Refresh()) |
| Приклад використання | File.Exists(path) | new FileInfo(path).Exists |
| Створення об'єкту | Не потрібно | Необхідно створити екземпляр |
Створіть програму, яка:
config.json у директорії settings.{"theme": "dark", "language": "uk"}.settings/backup/.Напишіть програму, яка знаходить файли з однаковими іменами (але в різних директоріях) всередині заданої папки. Використайте DirectoryInfo та рекурсивний пошук.
Створіть утиліту, яка видаляє всі .log файли старше 7 днів у вказаній директорії. Використайте FileInfo.LastWriteTime для перевірки дати.
Клас File
Клас Directory
Клас Path
FileInfo та DirectoryInfo
Path.Combine().UnauthorizedAccessException та IOException.