Привет, Хабр!
Работа с файлами — неотъемлемая часть многих проектов. Простая запись логов, чтение конфигурационных файлов или обработка больших данных — все это является частью взаимодействовия с файловой системой.
В этой статье, мы рассмотрим основы работы с файлами и потоками в C#.
Основные классы для работы с файлами и потоками в C#
File
и FileInfo
— это основные классы в пространстве имен System.IO
для работы с файловой системой. Оба класса предоставляют методы для создания, копирования, удаления, перемещения и открытия файлов, но есть пару различий.
File
— это статический класс, который предоставляет методы для выполнения операций с файлами. Подходит для простых операций с файлами, когда не требуется хранить состояние файла между вызовами методов.
Пример File
:
// создание нового файла
string filePath = @"C:\example.txt";
File.Create(filePath).Close();
// проверка существования файла
if (File.Exists(filePath))
{
Console.WriteLine("Файл существует.");
}
// запись текста в файл
string content = "Hello, World!";
File.WriteAllText(filePath, content);
// чтение текста из файла
string readContent = File.ReadAllText(filePath);
Console.WriteLine(readContent);
// копирование файла
string copyPath = @"C:\example_copy.txt";
File.Copy(filePath, copyPath);
// перемещение файла
string movePath = @"C:\example_moved.txt";
File.Move(copyPath, movePath);
// удаление файла
File.Delete(movePath);
FileInfo
сохраняет состояние файла, что делает его более хорошим при выполнении нескольких операций на одном и том же файле.
Пример:
// создание экземпляра FileInfo
FileInfo fileInfo = new FileInfo(filePath);
// создание нового файла
using (FileStream fs = fileInfo.Create())
{
byte[] info = new UTF8Encoding(true).GetBytes("This is some text in the file.");
fs.Write(info, 0, info.Length);
}
// проверка существования файла
if (fileInfo.Exists)
{
Console.WriteLine("Файл существует.");
}
// запись текста в файл
using (StreamWriter writer = fileInfo.AppendText())
{
writer.WriteLine("Добавленный текст.");
}
// чтение текста из файла
using (StreamReader reader = fileInfo.OpenText())
{
string s = "";
while ((s = reader.ReadLine()) != null)
{
Console.WriteLine(s);
}
}
// копирование файла
string copyFileInfoPath = @"C:\example_copy_info.txt";
fileInfo.CopyTo(copyFileInfoPath);
// перемещение файла
string moveFileInfoPath = @"C:\example_moved_info.txt";
fileInfo.MoveTo(moveFileInfoPath);
// удаление файла
fileInfo.Delete();
FileStream
— используется для работы с файлами на более низком уровне.
Пример FileStream
:
string fileStreamPath = @"C:\filestream_example.txt";
// создание и запись в файл
using (FileStream fs = new FileStream(fileStreamPath, FileMode.Create))
{
byte[] data = Encoding.UTF8.GetBytes("Hello, FileStream!");
fs.Write(data, 0, data.Length);
}
// чтение из файла
using (FileStream fs = new FileStream(fileStreamPath, FileMode.Open, FileAccess.Read))
{
byte[] data = new byte[fs.Length];
int numBytesToRead = (int)fs.Length;
int numBytesRead = 0;
while (numBytesToRead > 0)
{
int n = fs.Read(data, numBytesRead, numBytesToRead);
if (n == 0)
break;
numBytesRead += n;
numBytesToRead -= n;
}
string text = Encoding.UTF8.GetString(data);
Console.WriteLine(text);
}
// асинхронное чтение и запись
async Task WriteAsync(string path, string content)
{
byte[] encodedText = Encoding.UTF8.GetBytes(content);
using (FileStream sourceStream = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.None, 4096, true))
{
await sourceStream.WriteAsync(encodedText, 0, encodedText.Length);
};
}
async Task<string> ReadAsync(string path)
{
using (FileStream sourceStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true))
{
byte[] buffer = new byte[1024];
int numRead;
StringBuilder sb = new StringBuilder();
while ((numRead = await sourceStream.ReadAsync(buffer, 0, buffer.Length)) != 0)
{
string text = Encoding.UTF8.GetString(buffer, 0, numRead);
sb.Append(text);
}
return sb.ToString();
}
}
// пример использования асинхронного метода
await WriteAsync(fileStreamPath, "Async Hello, FileStream!");
string asyncContent = await ReadAsync(fileStreamPath);
Console.WriteLine(asyncContent);
Directory
и DirectoryInfo
предоставляют методы для создания, удаления и перечисления содержимого директорий. Как и File
и FileInfo
, Directory
— это статический класс, а DirectoryInfo
— это экземплярный класс.
Пример Directory
:
// создание новой директории
string directoryPath = @"C:\example_dir";
Directory.CreateDirectory(directoryPath);
// проверка существования директории
if (Directory.Exists(directoryPath))
{
Console.WriteLine("Директория существует.");
}
// перечисление файлов в директории
string[] files = Directory.GetFiles(directoryPath);
foreach (string file in files)
{
Console.WriteLine(file);
}
// перечисление поддиректорий
string[] directories = Directory.GetDirectories(directoryPath);
foreach (string dir in directories)
{
Console.WriteLine(dir);
}
// удаление директории
Directory.Delete(directoryPath, true);
ПримерDirectoryInfo
:
// создание экземпляра DirectoryInfo
DirectoryInfo directoryInfo = new DirectoryInfo(directoryPath);
// создание новой директории
directoryInfo.Create();
// проверка существования директории
if (directoryInfo.Exists)
{
Console.WriteLine("Директория существует.");
}
// перечисление файлов в директории
FileInfo[] fileInfos = directoryInfo.GetFiles();
foreach (FileInfo fileInfo in fileInfos)
{
Console.WriteLine(fileInfo.Name);
}
// перечисление поддиректорий
DirectoryInfo[] directoryInfos = directoryInfo.GetDirectories();
foreach (DirectoryInfo dirInfo in directoryInfos)
{
Console.WriteLine(dirInfo.Name);
}
// удаление директории
directoryInfo.Delete(true);
Чтение и запись файлов с FileStream
Создание и открытие файлов с помощью FileStream
включает использование различных режимов FileMode
, доступа FileAccess
и совместного использования FileShare
.
Примеры создания и открытия файлов:
// создание нового файла
string path = @"C:\example.txt";
using (FileStream fs = new FileStream(path, FileMode.Create))
{
byte[] info = new UTF8Encoding(true).GetBytes("This is some text in the file.");
fs.Write(info, 0, info.Length);
}
// открытие существующего файла для чтения
using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read))
{
byte[] b = new byte[1024];
UTF8Encoding temp = new UTF8Encoding(true);
while (fs.Read(b, 0, b.Length) > 0)
{
Console.WriteLine(temp.GetString(b));
}
}
FileMode.Create
используется для создания нового файла, который будет перезаписан, если он уже существует. FileMode.Open
открывает существующий файл, если он не существует, будет выброшено исключение FileNotFoundException
.
Чтение данных:
Метод Read
используется для чтения байтов из потока и записи их в заданный буфер.
using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read))
{
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
Console.WriteLine(Encoding.UTF8.GetString(buffer, 0, bytesRead));
}
}
Запись данных:
Метод Write
используется для записи массива байтов в файл.
using (FileStream fs = new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write))
{
byte[] info = new UTF8Encoding(true).GetBytes("More text to add to the file.");
fs.Write(info, 0, info.Length);
}
Работа с различными режимами доступа и совместного использования:
FileAccess
определяет уровень доступа к файлу: только чтение FileAccess.Read
, только запись FileAccess.Write
, или чтение и запись FileAccess.ReadWrite
.
FileShare
контролирует доступ других потоков к файлу: например, FileShare.Read
позволяет другим потокам читать файл, но не записывать в него.
// открытие файла для чтения и записи, с возможностью одновременного чтения другими потоками
using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.Read))
{
// чтение данных
byte[] buffer = new byte[1024];
int bytesRead = fs.Read(buffer, 0, buffer.Length);
Console.WriteLine(Encoding.UTF8.GetString(buffer, 0, bytesRead));
// запись данных
byte[] info = new UTF8Encoding(true).GetBytes("Additional data");
fs.Write(info, 0, info.Length);
}
Так можно юзать FileStream
с FileAccess.ReadWrite
для одновременного чтения и записи, при этом позволяя другим потокам читать файл с помощью FileShare.Read
.
Работа с потоками данных в C#
В .NET Stream
представляет собой абстрактный класс, который предоставляет универсальный интерфейс для чтения и записи байтов. Все потоки данных в .NET наследуются от Stream
, включая FileStream
, MemoryStream
, NetworkStream
и другие.
Основные методы Stream
:
Read(byte[] buffer, int offset, int count)
: читает байты из потока и записывает их в буфер.Write(byte[] buffer, int offset, int count)
: записывает байты из буфера в поток.Seek(long offset, SeekOrigin origin)
: перемещает текущую позицию в потоке.Flush()
: очищает все буферы для текущего потока.
Пример использования Stream
:
using System;
using System.IO;
class Program
{
static void Main()
{
string path = "example.txt";
using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write))
{
byte[] info = new UTF8Encoding(true).GetBytes("Hello, Stream!");
fs.Write(info, 0, info.Length);
}
using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read))
{
byte[] b = new byte[1024];
UTF8Encoding temp = new UTF8Encoding(true);
while (fs.Read(b, 0, b.Length) > 0)
{
Console.WriteLine(temp.GetString(b));
}
}
}
}
StreamReader
и StreamWriter
являются классами для работы с текстовыми данными.
Пример использования StreamWriter:
using System;
using System.IO;
class Program
{
static void Main()
{
string path = "example.txt";
using (StreamWriter sw = new StreamWriter(path))
{
sw.WriteLine("Hello, StreamWriter!");
}
using (StreamReader sr = new StreamReader(path))
{
string line;
while ((line = sr.ReadLine()) != null)
{
Console.WriteLine(line);
}
}
}
}
BinaryReader
и BinaryWriter
используются для чтения и записи данных в бинарном формате. Эти классы хороши для работы с примитивными типами данных.
Пример:
using System;
using System.IO;
class Program
{
static void Main()
{
string path = "example.bin";
using (BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)))
{
writer.Write(1.25);
writer.Write("Hello, BinaryWriter!");
writer.Write(true);
}
using (BinaryReader reader = new BinaryReader(File.Open(path, FileMode.Open)))
{
Console.WriteLine(reader.ReadDouble());
Console.WriteLine(reader.ReadString());
Console.WriteLine(reader.ReadBoolean());
}
}
}
Асинхронные операции позволяют выполнять задачи ввода-вывода без блокировки основного потока.
Пример асинхронной записи с StreamWriter:
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string path = "example.txt";
using (StreamWriter sw = new StreamWriter(path, true, Encoding.UTF8))
{
await sw.WriteLineAsync("Hello, Async StreamWriter!");
}
using (StreamReader sr = new StreamReader(path))
{
string line;
while ((line = await sr.ReadLineAsync()) != null)
{
Console.WriteLine(line);
}
}
}
}
Мониторинг изменений
FileSystemWatcher
позволяет следить за созданием, изменением, удалением и переименованием файлов и директорий.
Для начала создадим экземпляр FileSystemWatcher
, указав директорию для мониторинга:
using System;
using System.IO;
public class FileSystemWatcherExample
{
private FileSystemWatcher _watcher;
public FileSystemWatcherExample(string path)
{
_watcher = new FileSystemWatcher(path);
_watcher.IncludeSubdirectories = true; // следить за подкаталогами
_watcher.Filter = "*.*"; // следить за всеми файлами
_watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName |
NotifyFilters.Attributes | NotifyFilters.Size |
NotifyFilters.LastWrite | NotifyFilters.LastAccess |
NotifyFilters.CreationTime | NotifyFilters.Security;
// Подписка на события
_watcher.Created += OnCreated;
_watcher.Deleted += OnDeleted;
_watcher.Changed += OnChanged;
_watcher.Renamed += OnRenamed;
_watcher.Error += OnError;
_watcher.EnableRaisingEvents = true; // включение мониторинга
}
private void OnCreated(object sender, FileSystemEventArgs e) =>
Console.WriteLine($"File created: {e.FullPath}");
private void OnDeleted(object sender, FileSystemEventArgs e) =>
Console.WriteLine($"File deleted: {e.FullPath}");
private void OnChanged(object sender, FileSystemEventArgs e) =>
Console.WriteLine($"File changed: {e.FullPath}");
private void OnRenamed(object sender, RenamedEventArgs e) =>
Console.WriteLine($"File renamed: {e.OldFullPath} to {e.FullPath}");
private void OnError(object sender, ErrorEventArgs e) =>
PrintException(e.GetException());
private void PrintException(Exception ex)
{
if (ex != null)
{
Console.WriteLine($"Message: {ex.Message}");
Console.WriteLine("Stacktrace:");
Console.WriteLine(ex.StackTrace);
PrintException(ex.InnerException);
}
}
}
Чтобы более точно настроить, какие изменения отслеживать, есть свойства Filter
и NotifyFilter
:
Filter
позволяет отслеживать изменения только определенных типов файлов, например, только текстовых файлов*.txt
.NotifyFilter
позволяет указывать типы изменений.
Пример настройки фильтров:
_watcher.Filter = "*.txt";
_watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.Size | NotifyFilters.LastWrite;
События FileSystemWatcher
включают Created
, Deleted
, Changed
, Renamed
и Error
. Примеры обработки этих событий:
private void OnCreated(object sender, FileSystemEventArgs e) =>
Console.WriteLine($"File created: {e.FullPath}");
private void OnDeleted(object sender, FileSystemEventArgs e) =>
Console.WriteLine($"File deleted: {e.FullPath}");
private void OnChanged(object sender, FileSystemEventArgs e) =>
Console.WriteLine($"File changed: {e.FullPath}");
private void OnRenamed(object sender, RenamedEventArgs e) =>
Console.WriteLine($"File renamed: {e.OldFullPath} to {e.FullPath}");
private void OnError(object sender, ErrorEventArgs e) =>
PrintException(e.GetException());
private void PrintException(Exception ex)
{
if (ex != null)
{
Console.WriteLine($"Message: {ex.Message}");
Console.WriteLine("Stacktrace:");
Console.WriteLine(ex.StackTrace);
PrintException(ex.InnerException);
}
}
Будет хорошо, если вы увеличите размер внутреннего буфера с помощью свойства InternalBufferSize
, если планируете отслеживать большое количество изменений. Однако максимальный размер буфера ограничен 64 КБ.
В заключение напоминаю про открытый урок, посвященный исключениям и нюансам работы с ними, который пройдет 18 июля.
На уроке обсудим, что такое исключения и то, как мы их можем отловить и обработать. Рассмотрим общие и некоторые частные случаи по работе с исключительными ситуациями в .NET. Записаться на урок можно на странице курса "C# Developer. Professional".
Комментарии (7)
Politura
10.07.2024 15:36+2Интересно, для кого все это? Если для самых маленьких, которые не умеют читать файл, то они и не поймут нихрена, что в этих кусках кода написано, разбавленными скупыми комментариями. Зачем нужны потоки, когда можно хоба и ReadAllText одной командой сделать. Что за хрень такая async, и utf8. В чем отличие BinaryReader/BinaryWriter от StreamReader/StreamWriter - ведь и там и там байты. Ну и еще то, что наверняка взорвет мозг новичку: и Stream, и Thread по-русски называть одним и тем-же словом: потоки. :)
Ну а для тех, кто знает ответы на эти вопросы, они и с файлами работать уже давно умеют. В общем, не понятно, для кого именно статья написана.
Rusya1
10.07.2024 15:36На самом деле прикольно,респект за старания,но можно конкретность ввести,мол когда и что лучше использовать,я по крайне мере этого не понимаю
homesoft
10.07.2024 15:36Интересно бы понять преимущества async операций ввода-вывода. Ведь данные диск нам всё-равно быстрее, чем может, не вернёт. Соответственно, продолжать дальше мы по-любому не можем. Ну ладно, мы освободили текущий поток для чего-то ещё. Если этого чего-то ещё нет - то и освобождать некому. Если наш дргуой код (который не ждёт данные, а делает что-то ещё) мы и так забросили в другой поток, тут опять мы ничего не выигрываем. А async вызов - это лишнее переключение контекста и просто так не даётся (ну смотря как оно реализовано, может быть через WinAPI overlapped i/o).
Есть там ещё какая-то заморочка со therad pool когда мы реализуем web сервис например. Вместо создание нового потока для нового запроса фреймворк может как-то заиспользовать уже имеющийся, который висит на ожидании данных. Но это всё туманно. Может есть benchmark'и какие-нибудь с циферками, где можно выйгрыш посмотреть?
VanKrock
10.07.2024 15:36Через стримы итак все умеют, лучше бы рассказали как это правильно через Pipe делать
artemfisher
В чем смысл писать 1001 пособие?
bromzh
Реклама их сраных курсов. Берёшь туториал, который хоть как-то относится к теме курса -> пересказываешь/переводишь его на русский -> в конце ссылку на курс -> PROFIT. Они спамят по 3-4 статьи в день.