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);
}

Для больших файлов используйте стримы и читайте по частям, чтобы не съесть всю память.

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