JavaRush /Курси /C# SELF /Безпечне читання та запис файлів

Безпечне читання та запис файлів

C# SELF
Рівень 38 , Лекція 4
Відкрита

1. Вступ

У програмуванні під безпекою роботи з файлами розуміють не лише захист від вірусів і хакерів, а й грамотне поводження з помилками, блокуваннями, правами доступу, некоректними шляхами та іншими підводними каменями. Недбалий підхід може призвести до втрати даних, зависання програми, незрозумілих помилок або до сумнозвісного винятку IOException.

Ось типові ситуації, які вам варто вміти обробляти:

  • Файл не існує, але ви намагаєтеся його прочитати (або навпаки: файл уже створено, а ви хочете писати в режимі «створити, лише якщо його немає»).
  • Файл відкрито іншою програмою й заблоковано.
  • У користувача немає прав на читання або запис до цього файлу чи теки.
  • Шлях до файлу некоректний або містить заборонені символи.
  • Операцію неочікувано перервано (наприклад, закінчився вільний простір на диску).
  • Погана практика: залишати відкриті файли, не закривати потоки.

На щастя, .NET надає всі інструменти для розвʼязання цих проблем. Вони прості, але, як і з ременями безпеки, головне — не забувати їх застібати.

2. Базові принципи безпечної роботи з файлами

Перевіряйте існування файлу та права доступу заздалегідь

Перш ніж читати файл, перевірте, що він існує (наприклад, через File.Exists), особливо якщо шлях отримуєте від користувача:

string path = "test.txt";
if (!File.Exists(path))
{
    Console.WriteLine("Помилка: файл не знайдено.");
    return;
}

Перед записом переконайтеся, що тека існує та у вас є права на запис (або передайте помилки на рівень вище).

Ніколи не залишайте потоки відкритими

Використовуйте ключове слово using для автоматичного закриття потоків — це правильний підхід:

using var writer = new StreamWriter("output.txt");
writer.WriteLine("Hello, files!");
// Тут файл уже закрито, навіть якщо сталася помилка!

Це захищає від «завислих» блокувань і витоків ресурсів.

Завжди перехоплюйте винятки

Будь-яка файлова операція може згенерувати виняток. Навіть якщо файл щойно був на місці, його можуть видалити або перемістити в момент звернення. Використовуємо конструкцію try-catch:

try
{
    using var reader = new StreamReader("data.txt");
    string line = reader.ReadLine();
    Console.WriteLine(line);
}
catch (FileNotFoundException)
{
    Console.WriteLine("Файл не знайдено.");
}
catch (UnauthorizedAccessException)
{
    Console.WriteLine("Немає доступу до файлу.");
}
catch (IOException ex)
{
    Console.WriteLine($"Помилка введення-виведення: {ex.Message}");
}

Не довіряйте користувацькому введенню

Якщо шлях до файлу задає користувач, його можуть ввести з помилками (або навіть навмисно спричинити збій у програмі). Рекомендуємо перевіряти шлях (див. Path.GetInvalidPathChars()):

try
{
    string userPath = Console.ReadLine()!;
    if (string.IsNullOrWhiteSpace(userPath))
    {
        Console.WriteLine("Шлях не може бути порожнім!");
        return;
    }
    // Додатково: перевірити заборонені символи
    foreach (char c in Path.GetInvalidPathChars())
        if (userPath.Contains(c))
        {
            Console.WriteLine("Шлях містить заборонені символи.");
            return;
        }
    // Далі безпечно працювати з файлом
}
catch (Exception ex)
{
    Console.WriteLine("Помилка під час перевірки шляху: " + ex.Message);
}

3. Практичний приклад: читаємо файл як слід

Удосконалимо наш навчальний проєкт. Уявімо, що треба прочитати файл з імʼям, введеним користувачем, і вивести його вміст на екран. Усе це — з обробкою помилок і з урахуванням кодування.

Console.Write("Введіть шлях до файлу: ");
string? path = Console.ReadLine();

// Валідація шляху
if (string.IsNullOrWhiteSpace(path))
{
    Console.WriteLine("Шлях не може бути порожнім!");
    return;
}
foreach (char c in Path.GetInvalidPathChars())
    if (path.Contains(c))
    {
        Console.WriteLine("Шлях містить заборонені символи.");
        return;
    }

// Спробуємо прочитати файл
try
{
    if (!File.Exists(path))
    {
        Console.WriteLine("Файл не знайдено.");
        return;
    }

    // Явно вкажемо кодування (наприклад, UTF-8)
    using var reader = new StreamReader(path, Encoding.UTF8);
    string content = reader.ReadToEnd();
    Console.WriteLine("Вміст файлу:");
    Console.WriteLine(content);
}
catch (UnauthorizedAccessException)
{
    Console.WriteLine("Немає прав для читання файлу.");
}
catch (IOException ex)
{
    Console.WriteLine("Помилка введення-виведення: " + ex.Message);
}
catch (Exception ex)
{
    Console.WriteLine("Непередбачена помилка: " + ex.Message);
}

Зверніть увагу, цей код не завершиться аварійно, якщо файл зникне між введенням імʼя та читанням: виняток буде перехоплено. Потік автоматично закриється після виходу з блоку using — навіть якщо сталася помилка.

4. Файли на «запис»: уникаємо втрати даних

Коли відкриваємо файл на запис, особливо в режимі перезапису (false у другому параметрі конструктора StreamWriter), є ризик випадково стерти важливі дані. Ось кілька порад:

Перевіряйте — чи не перетираєте ви наявний файл

Іноді корисно спитати користувача, якщо файл уже існує:

if (File.Exists(path))
{
    Console.WriteLine("Увага: файл уже існує. Перезаписати? (y/n)");
    string answer = Console.ReadLine()!;
    if (!answer.Equals("y", StringComparison.OrdinalIgnoreCase))
        return;
}

Використовуйте режим «append» (допис) там, де це потрібно

using var writer = new StreamWriter("log.txt", append: true);
writer.WriteLine(DateTime.Now + ": новий запис у журналі.");

Стара інформація не зникне.

5. Захист від гонок і конфліктів

Іноді файл можуть одночасно відкрити кілька програм (наприклад, ваш C#‑клієнт і, скажімо, Notepad++). Це може призвести до помилок. За замовчуванням StreamReader і StreamWriter використовують режими спільного доступу на базі FileShare — тобто або дозволяють читання, або його забороняють.

Можна керувати цим явно:

using var stream = new FileStream("data.txt", FileMode.Open, FileAccess.Read, FileShare.Read);
using var reader = new StreamReader(stream, Encoding.UTF8);
// Читання...
  • FileShare.Read: хтось інший може лише читати.
  • FileShare.None: жодного спільного доступу — файл повністю «твій».

Якщо треба, щоб файл могли одночасно читати й писати різні програми — використовуйте відповідний режим, але обережно: так зазвичай роблять лише тоді, коли добре розуміють наслідки.

6. Неочікувані винятки та що з ними робити

Навіть якщо суворо дотримуватися всіх порад, можуть траплятися збої, повʼязані, наприклад, із нестачею місця на диску, зламаною флешкою або вірусом. Ось кілька «екзотичних» винятків та поради, як їх обробляти:

  • PathTooLongException — шлях до файлу надто довгий (понад 260 символів у старих версіях Windows).
  • DirectoryNotFoundException — вказану теку не знайдено.
  • DriveNotFoundException — наприклад, якщо шлях "Z:\\file.txt", а диска Z не існує.
  • NotSupportedException — наприклад, шлях містить неприпустиму комбінацію.

Рекомендуємо мати окремі блоки для таких винятків — принаймні фіксувати їх у журналі окремо.

7. Використовуємо тимчасові файли для атомарного запису

Класична проблема: записуємо файл, але програма аварійно завершується посередині — маємо пошкоджений файл. Професійні програми часто використовують стратегію «атомарного» запису:

  1. Записати вміст у тимчасовий файл (наприклад, "file.txt.tmp").
  2. Перемістити тимчасовий файл (операція зазвичай атомарна на рівні файлової системи) на місце цільового (File.Replace).
  3. Старий файл замінено повністю або не змінено зовсім — «напівпошкоджених» даних не буває.

Приклад:

string tempPath = path + ".tmp";
try
{
    using var writer = new StreamWriter(tempPath, false, Encoding.UTF8);
    // Записуємо усе у тимчасовий файл
    writer.Write(contentForSave);
    // Після успішного запису замінюємо основний файл
    File.Replace(tempPath, path, null); // Переміщує тимчасовий файл, замінюючи цільовий (атомарна операція)
}
catch (Exception ex)
{
    Console.WriteLine("Помилка збереження файлу: " + ex.Message);
    // Файл tempPath бажано видалити, якщо не потрібен
}

На практиці так роблять багато редакторів і офісних пакетів, щоб гарантувати цілісність даних.

8. Використовуємо класи-обгортки для безпечного доступу

.NET має допоміжні методи для безпечних файлових операцій. Наприклад, File.ReadAllText і File.WriteAllText автоматично відкривають, читають або пишуть і закривають файл. Але навіть їх варто обгортати в try-catch:

try
{
    string text = File.ReadAllText("settings.json", Encoding.UTF8);
    // Працюємо з даними...
}
catch (Exception ex)
{
    Console.WriteLine("Помилка роботи з файлом: " + ex.Message);
}

Для великих файлів використовуйте потоки й читайте частинами, щоб не вичерпати всю памʼять.

1
Опитування
Типові виключення, рівень 38, лекція 4
Недоступний
Типові виключення
Обробка помилок при роботі з файлами
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ