В пространстве имён System.IO есть всё для работы с файлами. Для большинства задач достаточно одного вызова статического класса File: он открывает файл, читает или записывает его и закрывает. Для больших файлов или тонкого управления используйте StreamReader и StreamWriter. Добавьте using System.IO; в каждый пример ниже.
Запись и чтение целого файла
File.WriteAllText создаёт файл (или перезаписывает существующий) с переданным текстом. File.ReadAllText возвращает весь файл одной строкой. File.AppendAllText дописывает в конец:
Вывод:
True
Buy coffee
Call the dentist
Renew passport
43 characters
Start over
False
Относительный путь вроде "notes.txt" разрешается относительно текущего каталога процесса, и это не всегда папка, где лежит программа (служба, запланированная задача или средство запуска тестов могут стартовать в другом месте). Для файлов, которые поставляются с приложением, стройте путь от AppContext.BaseDirectory.
WriteAllText перезаписывает без вопросов. Если потеря старого содержимого важна, сначала проверьте File.Exists или запишите во временный файл, а затем переместите его на место через File.Move.
Строки: WriteAllLines, ReadAllLines и ReadLines
Файлы с записями, по одной на строку, встречаются достаточно часто, чтобы для них были отдельные методы. WriteAllLines записывает каждую строку с переводом строки после неё; ReadAllLines возвращает string[]:
Вывод:
4 lines, header: date,product,amount
Total: 45.60
2026-03-03,Stapler,7.00
Разница между двумя способами чтения важна для размера. ReadAllLines читает весь файл в память до возврата. ReadLines возвращает ленивый IEnumerable<string>, который читает по мере перебора, поэтому foreach по логу в 5 ГБ использует несколько килобайт. С LINQ ReadLines(...).Where(...).Take(10) прекращает чтение, получив десять совпадений.
Разбор CSV через Split(',') работает для простых файлов, которые вы создали сами. В реальном CSV бывают поля в кавычках, содержащие запятые; для них используйте библиотеку вроде CsvHelper.
Обратите внимание на InvariantCulture в decimal.Parse: без неё разбор "4.50" на машине с немецкими или португальскими настройками прочитает точку как разделитель тысяч.
StreamWriter и StreamReader
Методы File открывают и закрывают файл при каждом вызове. Чтобы со временем записать много частей или прочитать файл, слишком большой для памяти, без LINQ, откройте поток один раз. Всегда оборачивайте его в using, чтобы он закрылся даже при исключении; незакрытое средство записи может так и не сбросить буфер, и файл окажется пустым или обрезанным.
Вывод:
2: WARN disk 85% full
4: ERROR connection refused
ReadLine возвращает null в конце файла, и это завершает цикл while. StreamWriter буферизует вывод и записывает его на диск, когда буфер заполняется, когда вы вызываете Flush() и когда его освобождают; блок using гарантирует последнее.
Начиная с C# 8 объявление using освобождает поток в конце объемлющего блока без лишних фигурных скобок:
using var writer = new StreamWriter(path); // C# 8: disposed when the method returns
writer.WriteLine("INFO server started");
Пути и каталоги
Стройте пути через Path.Combine, а не склеиванием строк. Он вставляет правильный разделитель для операционной системы (\ в Windows, / в Linux и macOS) и не удваивает его. Класс Path также разбирает пути на части, а Directory создаёт папки и перечисляет их содержимое:
Вывод:
february.txt, january.txt, march.txt
march.txt
march
.txt
march.pdf
False
Одна ловушка: если какой-то из следующих аргументов Path.Combine начинается с корня (с / или \, или с буквы диска в Windows), всё до него отбрасывается, поэтому Path.Combine("reports", "/2026") даёт /2026. Передавайте относительные части или используйте Path.Join (.NET Core 3.0 и новее), который никогда ничего не отбрасывает.
Directory.GetFiles возвращает файлы в том порядке, в каком их отдаёт файловая система, а он различается в Windows и Linux, поэтому сортируйте результат, когда порядок важен. Directory.EnumerateFiles это ленивая версия, в таком же отношении, как ReadLines к ReadAllLines. Передайте SearchOption.AllDirectories, чтобы включить подпапки.
Обработка ошибок
Файловые операции завершаются ошибкой по причинам вне вашей программы: файла нет, папка не существует, другой процесс держит файл открытым, диск заполнен, неверные права доступа. У каждой причины свой тип исключения. FileNotFoundException и DirectoryNotFoundException наследуются от IOException, поэтому перехватывайте их раньше него; UnauthorizedAccessException не наследуется, поэтому ему нужен собственный catch:
Вывод:
missing file, using defaults
missing folder, using defaults
theme=dark
File.Exists перед чтением кажется проще, но не отменяет необходимость try: файл может исчезнуть или быть заблокирован другой программой в промежутке между проверкой и чтением. Используйте Exists, чтобы решить, что делать, а catch, чтобы пережить то, что происходит на самом деле.
Кодировка
File.WriteAllText, WriteAllLines и StreamWriter по умолчанию пишут UTF-8 без метки порядка байтов (BOM), а средства чтения распознают UTF-8, UTF-16 и UTF-32 по метке, если она есть. Передайте Encoding, когда файл должен быть в определённом формате:
Вывод:
plain.txt 16 bytes, reads back unchanged: True
bom.txt 19 bytes, reads back unchanged: True
utf16.txt 30 bytes, reads back unchanged: True
é и ã занимают в UTF-8 по два байта, поэтому 14 символам нужно 16 байт, а все три файла читаются правильно, потому что средство чтения распознаёт метку порядка байтов. Encoding.UTF8 выглядит как значение по умолчанию, но добавляет метку порядка байтов, которую некоторые инструменты (старые импортёры CSV, скрипты оболочки) показывают как мусор в начале первой строки; используйте new UTF8Encoding(false) для явного UTF-8 без неё. Чтение файла в неверной кодировке не выбрасывает исключение: получаются символы замены, поэтому выясняйте, в какой кодировке файл, который создали не вы.
Асинхронный файловый ввод-вывод
В веб-приложениях и коде интерфейса блокировать поток на дисковом вводе-выводе расточительно. В .NET Core 2.0 и новее есть асинхронные версии методов File:
await File.WriteAllTextAsync("notes.txt", text);
string content = await File.ReadAllTextAsync("notes.txt");
string[] lines = await File.ReadAllLinesAsync("sales.csv");
StreamReader.ReadLineAsync и StreamWriter.WriteLineAsync работают так же с потоками.
Частые ошибки
- Неосвобождённый поток. Без
usingбуферизованный текст может не дойти до диска, а файл остаётся заблокированным, пока не отработает сборщик мусора. - Сборка путей через
+ "\\" +. Ломается в Linux и macOS. ИспользуйтеPath.Combine. - Загрузка огромных файлов через
ReadAllTextилиReadAllLines. Читайте их потоково черезReadLinesилиStreamReader. - Опора на текущий каталог. Он зависит от того, как запущена программа. Используйте абсолютные пути или
AppContext.BaseDirectory. - Разбор чисел из файлов с культурой машины. Для файлов данных передавайте
CultureInfo.InvariantCulture. - Ожидание, что
File.Existsпредотвратит исключения. Файл может измениться между проверкой и использованием.
Часто задаваемые вопросы
Как записать текст в файл в C#?
File.WriteAllText("notes.txt", text); создаёт файл или перезаписывает существующий, записывает строку в UTF-8 и закрывает его. Используйте File.WriteAllLines(path, lines) для коллекции строк и File.AppendAllText(path, text), чтобы дописать в конец, а не заменить. Все три находятся в System.IO.
Как прочитать текстовый файл в C#?
string text = File.ReadAllText("notes.txt"); читает весь файл в одну строку, а string[] lines = File.ReadAllLines(path); разбивает его на строки. Для больших файлов используйте File.ReadLines(path) в foreach, который читает по одной строке, а не загружает всё, или StreamReader.
Как дописать данные в файл в C#?
File.AppendAllText(path, text) добавляет текст в конец файла и создаёт файл, если его нет; File.AppendAllLines(path, lines) делает то же для строк. С потоком откройте его в режиме дозаписи: new StreamWriter(path, append: true). При дозаписи текста не забудьте самостоятельно добавить Environment.NewLine или \n.
Как читать файл построчно в C#?
foreach (string line in File.ReadLines(path)) { ... } читает файл по одной строке, поэтому расход памяти остаётся ровным даже для гигабайтных логов. Более старый эквивалент это StreamReader в блоке using с while ((line = reader.ReadLine()) != null). Для больших файлов избегайте ReadAllLines: он сначала загружает все строки в массив.
Как проверить, существует ли файл, в C#?
File.Exists(path) возвращает true, если файл существует и программа может его видеть, а Directory.Exists(path) делает то же для папок. Не полагайтесь на это, чтобы избежать исключений: файл могут удалить или заблокировать между проверкой и чтением, поэтому всё равно обрабатывайте FileNotFoundException и IOException вокруг самой операции с файлом.