🔷 C#

Работа с файлами в C# (.NET): подробное руководство

Что это такое и зачем? (аналогия из реальной жизни)

Представьте, что вы пишете письмо другу. Вы берёте лист бумаги, пишете текст, складываете его в конверт и отправляете. Через неделю друг открывает конверт и читает ваше послание. В мире программирования файл — это тот самый лист бумаги, а ваша программа — это и автор письма, и читатель одновременно.

Файлы бывают разные: текстовые (как дневник), бинарные (как фотография), структурированные (как таблица Excel). Но суть одна: сохранить информацию надолго, чтобы она не исчезла при выключении компьютера.

Зачем программисту уметь работать с файлами?

  • Сохранять настройки — чтобы игра запомнила ваш уровень и количество жизней.
  • Читать данные — загрузить список учеников из школьного журнала.
  • Логировать — записывать, когда и кто заходил в систему.
  • Обмениваться информацией — экспортировать отчёт в Excel или импортировать контакты.
💡 Важно: Работа с файлами делится на два больших этапа: ЧТЕНИЕ (мы забираем данные из файла) и ЗАПИСЬ (мы отправляем данные в файл). В этой статье мы подробно разберём чтение.

Теория: как это работает изнутри

Когда ваша программа хочет прочитать файл, происходит целый спектакль с участием операционной системы, жёсткого диска и самой программы. Давайте заглянем за кулисы.

Потоки (Streams) — главные герои

Представьте, что файл — это огромная труба, заполненная водой (данными). Чтобы напиться, вам нужна труба поменьше — поток. В C# любой файл открывается через поток (Stream). Поток — это последовательность байтов, которая течёт от источника к получателю.

Есть два типа потоков:

  • Байтовые потоки — работают с сырыми байтами (0 и 1). Подходят для изображений, видео, архивов.
  • Символьные потоки — работают с текстом, автоматически преобразуя байты в буквы (с учётом кодировки).

Кодировки — почему буквы превращаются в кракозябры

Компьютер не понимает буквы. Он понимает только числа. Кодировка — это таблица соответствия: буква «А» = число 65. Если вы запишете текст в одной кодировке (например, Windows-1251), а прочитаете в другой (UTF-8), то получите абракадабру.

Самая популярная кодировка сегодня — UTF-8. Она понимает все языки мира, включая эмодзи. В C# по умолчанию используется UTF-8, но лучше указывать явно.

Путь к файлу — адрес в мире компьютера

Чтобы открыть файл, нужно знать его адрес. Пути бывают:

  • Абсолютный — полный адрес от корня диска: C:UsersИванDocuments otes.txt
  • Относительный — адрес относительно папки с программой: datasettings.txt
⚠️ Совет: Всегда используйте относительные пути, если файл лежит рядом с программой. Так ваш код будет работать на любом компьютере.

Режимы открытия файла

Когда вы открываете файл, нужно сказать системе, что вы собираетесь делать:

  • FileMode.Open — открыть существующий файл (если файла нет — ошибка).
  • FileMode.OpenOrCreate — открыть существующий или создать новый.
  • FileMode.Append — открыть и дописать в конец.

Синтаксис в C# (.NET): все формы и варианты

В C# есть несколько способов прочитать файл. Мы рассмотрим все основные, от самых простых до более гибких.

1. Класс File — быстрый и простой

Этот класс содержит статические методы. Вам не нужно создавать объект — просто вызываете метод и получаете результат.

Метод Что делает Возвращает
File.ReadAllText(path) Читает весь файл одной строкой string
File.ReadAllLines(path) Читает все строки в массив string[]
File.ReadAllBytes(path) Читает все байты (для бинарных файлов) byte[]

2. Класс StreamReader — контроль над процессом

Если файл большой (миллионы строк), читать его целиком в память опасно — программа может упасть. StreamReader читает построчно, экономя память.

Метод Описание
ReadLine() Читает одну строку до символа новой строки
ReadToEnd() Читает всё до конца потока
Read() Читает один символ
Peek() Смотрит следующий символ, но не забирает его

3. Асинхронное чтение — для современных приложений

Если программа работает с интерфейсом (WPF, WinForms, ASP.NET), чтение файла не должно "замораживать" экран. Асинхронные методы решают эту проблему:

  • ReadAllTextAsync()
  • ReadLineAsync()
  • ReadToEndAsync()

4. Конструкция using — гарантия закрытия файла

Файл — это ресурс операционной системы. Если вы его откроете и забудете закрыть, другие программы не смогут к нему обратиться. using автоматически закрывает файл, даже если произошла ошибка.

// Без using — плохо, файл может остаться открытым
StreamReader reader = new StreamReader("file.txt");
string text = reader.ReadToEnd();
reader.Close(); // Если до этой строки будет ошибка, файл не закроется

// С using — правильно, файл закроется в любом случае
using (StreamReader reader = new StreamReader("file.txt"))
{
    string text = reader.ReadToEnd();
} // Здесь файл автоматически закроется

Примеры кода — от простого к сложному

Пример 1: Чтение всего текста одной строкой

using System;
using System.IO; // Обязательно подключить пространство имён для работы с файлами

class Program
{
    static void Main()
    {
        // Путь к файлу (файл должен лежать рядом с программой)
        string filePath = "hello.txt";
        
        // Читаем ВЕСЬ файл в одну строку
        // File.ReadAllText — самый простой способ
        string content = File.ReadAllText(filePath);
        
        // Выводим содержимое на экран
        Console.WriteLine("Содержимое файла:");
        Console.WriteLine(content);
    }
}

Объяснение: Метод File.ReadAllText открывает файл, читает всё содержимое, закрывает файл и возвращает строку. Идеально для небольших файлов (до 100 КБ).

Пример 2: Чтение построчно в массив

using System;
using System.IO;

class Program
{
    static void Main()
    {
        string filePath = "students.txt";
        
        // Читаем все строки в массив строк
        // Каждая строка файла — отдельный элемент массива
        string[] lines = File.ReadAllLines(filePath);
        
        Console.WriteLine("Список учеников:");
        // Проходим по всем строкам с помощью цикла foreach
        foreach (string line in lines)
        {
            Console.WriteLine($"- {line}");
        }
        
        // Узнаём, сколько строк в файле
        Console.WriteLine($"Всего строк: {lines.Length}");
    }
}

Объяснение: Если файл содержит список (например, имена учеников), удобно читать его в массив. Каждая строка становится отдельным элементом.

Пример 3: Чтение с помощью StreamReader (построчно)

using System;
using System.IO;

class Program
{
    static void Main()
    {
        string filePath = "big_log.txt";
        
        // Используем using, чтобы гарантированно закрыть файл
        using (StreamReader reader = new StreamReader(filePath))
        {
            string line;
            int lineNumber = 0;
            
            // Читаем строку за строкой, пока не достигнем конца файла
            // ReadLine возвращает null, когда строки закончились
            while ((line = reader.ReadLine()) != null)
            {
                lineNumber++;
                Console.WriteLine($"Строка {lineNumber}: {line}");
            }
        } // Здесь reader автоматически закрывается
    }
}

Объяснение: Этот способ не загружает весь файл в память. Если файл весит 10 ГБ, программа не упадёт — она обработает его по кусочкам.

Пример 4: Чтение с указанием кодировки

using System;
using System.IO;
using System.Text; // Для Encoding

class Program
{
    static void Main()
    {
        string filePath = "russian_text.txt";
        
        // Явно указываем кодировку UTF-8
        // Если не указать, C# использует UTF-8 по умолчанию,
        // но лучше быть явным
        using (StreamReader reader = new StreamReader(filePath, Encoding.UTF8))
        {
            string content = reader.ReadToEnd();
            Console.WriteLine(content);
        }
        
        // Альтернатива: чтение с кодировкой Windows-1251
        // Encoding.GetEncoding(1251) — для старых русских файлов
        using (StreamReader reader = new StreamReader(filePath, Encoding.GetEncoding(1251)))
        {
            string content = reader.ReadToEnd();
            Console.WriteLine("Тот же текст в Windows-1251:");
            Console.WriteLine(content);
        }
    }
}

Объяснение: Кодировка — частая причина ошибок. Если видите "кракозябры" (Привет), значит, кодировка не совпадает. Всегда указывайте Encoding.UTF8 для современных файлов.

Пример 5: Асинхронное чтение (не блокирует интерфейс)

using System;
using System.IO;
using System.Threading.Tasks; // Для async/await

class Program
{
    // Метод Main теперь асинхронный
    static async Task Main()
    {
        string filePath = "large_file.txt";
        
        Console.WriteLine("Начинаем чтение файла...");
        
        // Асинхронное чтение — программа не зависает
        // Можно представить, что читает "в фоне"
        string content = await File.ReadAllTextAsync(filePath);
        
        Console.WriteLine("Файл прочитан!");
        Console.WriteLine($"Длина текста: {content.Length} символов");
        
        // Асинхронное чтение построчно
        using (StreamReader reader = new StreamReader(filePath))
        {
            string line;
            while ((line = await reader.ReadLineAsync()) != null)
            {
                Console.WriteLine(line);
            }
        }
    }
}

Объяснение: async и await — это магия, которая позволяет программе не ждать, пока файл прочитается. Пока файл читается, программа может рисовать анимацию или отвечать на клики мыши.

Пример 6: Чтение бинарного файла (изображение)

using System;
using System.IO;

class Program
{
    static void Main()
    {
        string filePath = "photo.jpg";
        
        // Читаем все байты файла
        // Для изображений, музыки, видео — только так
        byte[] fileBytes = File.ReadAllBytes(filePath);
        
        Console.WriteLine($"Размер файла: {fileBytes.Length} байт");
        Console.WriteLine($"Это примерно {fileBytes.Length / 1024} КБ");
        
        // Можно посмотреть первые 10 байт (заголовок файла)
        Console.WriteLine("Первые 10 байт:");
        for (int i = 0; i < 10 && i < fileBytes.Length; i++)
        {
            Console.Write($"{fileBytes[i]:X2} "); // Выводим в шестнадцатеричном формате
        }
        Console.WriteLine();
    }
}

Объяснение: Бинарные файлы нельзя читать как текст — получите мусор. Используйте ReadAllBytes и работайте с массивом байтов.

Пример 7: Проверка существования файла и обработка ошибок

using System;
using System.IO;

class Program
{
    static void Main()
    {
        string filePath = "maybe_exists.txt";
        
        // Проверяем, существует ли файл
        if (File.Exists(filePath))
        {
            Console.WriteLine("Файл найден! Читаем...");
            string content = File.ReadAllText(filePath);
            Console.WriteLine(content);
        }
        else
        {
            Console.WriteLine($"Файл '{filePath}' не найден.");
            Console.WriteLine("Создаём файл с примером текста...");
            
            // Создаём файл и записываем в него текст
            File.WriteAllText(filePath, "Это новый файл!");
            Console.WriteLine("Файл создан. Запустите программу ещё раз.");
        }
        
        // Обработка исключений — защита от неожиданностей
        try
        {
            // Попытка прочитать файл, к которому нет доступа
            string secret = File.ReadAllText(@"C:WindowsSystem32configSAM");
        }
        catch (UnauthorizedAccessException)
        {
            Console.WriteLine("Ошибка: Нет прав доступа к этому файлу!");
        }
        catch (FileNotFoundException)
        {
            Console.WriteLine("Ошибка: Файл не найден!");
        }
        catch (IOException ex)
        {
            Console.WriteLine($"Ошибка ввода-вывода: {ex.Message}");
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Неизвестная ошибка: {ex.Message}");
        }
    }
}

Объяснение: Всегда проверяйте, существует ли файл, и используйте try-catch для обработки ошибок. Файл может быть удалён, занят другой программой или защищён от чтения.

Сравнение с Python: чем похоже и чем отличается

Если вы уже знаете Python, вам будет легче освоить C#. Давайте сравним основные подходы.

Операция C# Python
Чтение всего файла File.ReadAllText(path) open(path).read()
Чтение всех строк File.ReadAllLines(path) open(path).readlines()
Построчное чтение StreamReader.ReadLine() for line in file:
Контекстный менеджер using with open(...) as f:
Указание кодировки Encoding.UTF8 encoding='utf-8'
Асинхронное чтение ReadAllTextAsync() asyncio (сложнее)

Главные отличия:

  • Типизация: В C# нужно явно указывать тип переменной (string content), в Python — нет.
  • Производительность: C# быстрее читает большие файлы благодаря компиляции.
  • Безопасность: C# требует явного закрытия файла (через using), Python делает это автоматически при выходе из with.
  • Асинхронность: В C# асинхронное чтение встроено в стандартную библиотеку, в Python нужно подключать дополнительные модули.
💡 Совет: Если вы переходите с Python на C#, запомните главное правило: в C# всё строже и требует больше кода, но этот код работает надёжнее и быстрее.

Типичные ошибки и подводные камни

1. Файл не найден (FileNotFoundException)

Причина: Неправильный путь к файлу. Файла нет в указанном месте.

Решение: Всегда проверяйте путь. Используйте File.Exists() перед чтением. Помните, что относительный путь считается от папки с исполняемым файлом (обычно bin/Debug/net8.0).

2. Файл занят другим процессом (IOException)

Причина: Файл открыт в другой программе (блокнот, Excel, антивирус).

Решение: Закройте программу, которая использует файл. В коде используйте using, чтобы файл не оставался открытым.

3. Неправильная кодировка (кракозябры)

Причина: Файл сохранён в одной кодировке, а читается в другой.

Решение: Узнайте кодировку файла (обычно UTF-8 или Windows-1251). Указывайте её явно: new StreamReader(path, Encoding.UTF8).

4. Недостаточно прав (UnauthorizedAccessException)

Причина: Программа пытается читать системный файл или файл из защищённой папки.

Решение: Запускайте программу от имени администратора или читайте файлы из папки пользователя (Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)).

5. Слишком большой файл (OutOfMemoryException)

Причина: Использовали ReadAllText для файла размером 2 ГБ.

Решение: Для больших файлов используйте StreamReader и читайте построчно.

6. Забыли закрыть файл

Причина: Не использовали using или не вызвали Close().

Решение: Всегда оборачивайте работу с файлом в using. Это гарантирует закрытие даже при ошибке.

7. Путь содержит недопустимые символы

Причина: В имени файла есть символы / : * ? " < > |.

Решение: Проверяйте имя файла с помощью Path.GetInvalidFileNameChars().

Лучшие практики и советы профессионалов

1. Всегда используйте using

Это железное правило. using гарантирует, что файл будет закрыт, даже если произойдёт исключение. Никогда не открывайте файл без using.

2. Для больших файлов — только построчное чтение

Если файл больше 100 МБ, не используйте ReadAllText или ReadAllLines. Они загружают всё в память. Используйте StreamReader.ReadLine() в цикле.

3. Указывайте кодировку явно

Даже если вы уверены, что файл в UTF-8, укажите это. Код станет понятнее, и вы избежите проблем при переносе на другой компьютер.

4. Используйте относительные пути

Абсолютные пути (C:Users...) привязывают программу к конкретному компьютеру. Используйте относительные пути или получайте путь к папке пользователя через Environment.

5. Обрабатывайте исключения

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

6. Для конфигурации используйте JSON или XML

Не храните настройки в текстовых файлах с ручным парсингом. Используйте System.Text.Json или System.Xml — это надёжнее и удобнее.

7. Асинхронность — для UI, синхронность — для консоли

В консольных приложениях асинхронность не нужна. В приложениях с интерфейсом (WPF, WinForms, ASP.NET) используйте асинхронные методы, чтобы не блокировать UI.

8. Проверяйте существование файла перед чтением

Одна проверка File.Exists() может спасти от исключения. Но помните: между проверкой и чтением файл могут удалить. Поэтому try-catch всё равно нужен.

Вопросы для самопроверки

  1. Вопрос: Какой класс в C# используется для чтения текстовых файлов построчно?

    Ответ: StreamReader. Он позволяет читать файл по одной строке с помощью метода ReadLine().

  2. Вопрос: Что произойдёт, если использовать File.ReadAllText() для файла размером 5 ГБ?

    Ответ: Программа вызовет исключение OutOfMemoryException, так как попытается загрузить весь файл в оперативную память. Для больших файлов нужно использовать StreamReader.

  3. Вопрос: Зачем нужна конструкция using при работе с файлами?

    Ответ: using гарантирует, что файл будет закрыт после использования, даже если произойдёт исключение. Это предотвращает утечку ресурсов и блокировку файла.

  4. Вопрос: Как прочитать файл в кодировке Windows-1251?

    Ответ: Нужно указать кодировку при создании StreamReader: new StreamReader(path, Encoding.GetEncoding(1251)).

  5. Вопрос: В чём разница между File.ReadAllLines() и StreamReader.ReadLine()?

    Ответ: ReadAllLines() читает все строки сразу в массив (память). ReadLine() читает по одной строке (экономия памяти).

  6. Вопрос: Какой метод нужно использовать для чтения изображения?

    Ответ: File.ReadAllBytes(), так как изображение — это бинарный файл, и его нельзя читать как текст.

  7. Вопрос: Что такое асинхронное чтение и когда его применять?

    Ответ: Асинхронное чтение (методы с Async) не блокирует выполнение программы. Его нужно использовать в приложениях с графическим интерфейсом, чтобы экран не "зависал" во время чтения файла.

  8. Вопрос: Какие исключения могут возникнуть при чтении файла?

    Ответ: FileNotFoundException (файл не найден), UnauthorizedAccessException (нет прав), IOException (файл занят), OutOfMemoryException (слишком большой файл).

  9. Вопрос: Как проверить, существует ли файл, перед чтением?

    Ответ: Использовать метод File.Exists(path), который возвращает true или false.

  10. Вопрос: В чём отличие относительного пути от абсолютного?

    Ответ: Абсолютный путь начинается от корня диска (C:...), относительный — от текущей папки программы. Относительные пути предпочтительнее, так как программа будет работать на любом компьютере.

Итог и что изучить дальше

Мы прошли большой путь: от простой аналогии с письмом до асинхронного чтения бинарных файлов. Теперь вы знаете:

  • Что такое файлы и зачем они нужны.
  • Как работают потоки и кодировки.
  • Все основные методы чтения файлов в C#.
  • Как избегать типичных ошибок.
  • Лучшие практики профессионалов.

Что изучить дальше?

  1. Запись в файлы — научитесь сохранять данные. Методы File.WriteAllText(), StreamWriter.
  2. Работа с директориями — класс Directory для создания, удаления и обхода папок.
  3. Форматированный ввод/вывод — JSON, XML, CSV. Как читать структурированные данные.
  4. Потоки (Streams) в деталях — FileStream, MemoryStream, BufferedStream.
  5. Сериализация — как сохранить целый объект в файл одной командой.
🎯 Главный совет: Не бойтесь экспериментировать! Создайте текстовый файл, напишите программу, которая его читает, и меняйте код, чтобы увидеть, как работают разные методы. Практика — лучший учитель.

Помните: умение работать с файлами — это фундамент, на котором строится любое серьёзное приложение. Освоив его, вы сможете создавать программы, которые хранят историю, настройки, базы данных и целые миры. Удачи в программировании!