Привет, Хабр!

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

В этой статье, мы рассмотрим основы работы с файлами и потоками в 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)


  1. artemfisher
    10.07.2024 15:36
    +1

    В чем смысл писать 1001 пособие?


    1. bromzh
      10.07.2024 15:36
      +3

      Реклама их сраных курсов. Берёшь туториал, который хоть как-то относится к теме курса -> пересказываешь/переводишь его на русский -> в конце ссылку на курс -> PROFIT. Они спамят по 3-4 статьи в день.


  1. AccountForHabr
    10.07.2024 15:36

    Ещё и примеры плохому учат...


  1. Politura
    10.07.2024 15:36
    +2

    Интересно, для кого все это? Если для самых маленьких, которые не умеют читать файл, то они и не поймут нихрена, что в этих кусках кода написано, разбавленными скупыми комментариями. Зачем нужны потоки, когда можно хоба и ReadAllText одной командой сделать. Что за хрень такая async, и utf8. В чем отличие BinaryReader/BinaryWriter от StreamReader/StreamWriter - ведь и там и там байты. Ну и еще то, что наверняка взорвет мозг новичку: и Stream, и Thread по-русски называть одним и тем-же словом: потоки. :)

    Ну а для тех, кто знает ответы на эти вопросы, они и с файлами работать уже давно умеют. В общем, не понятно, для кого именно статья написана.


  1. Rusya1
    10.07.2024 15:36

    На самом деле прикольно,респект за старания,но можно конкретность ввести,мол когда и что лучше использовать,я по крайне мере этого не понимаю


  1. homesoft
    10.07.2024 15:36

    Интересно бы понять преимущества async операций ввода-вывода. Ведь данные диск нам всё-равно быстрее, чем может, не вернёт. Соответственно, продолжать дальше мы по-любому не можем. Ну ладно, мы освободили текущий поток для чего-то ещё. Если этого чего-то ещё нет - то и освобождать некому. Если наш дргуой код (который не ждёт данные, а делает что-то ещё) мы и так забросили в другой поток, тут опять мы ничего не выигрываем. А async вызов - это лишнее переключение контекста и просто так не даётся (ну смотря как оно реализовано, может быть через WinAPI overlapped i/o).

    Есть там ещё какая-то заморочка со therad pool когда мы реализуем web сервис например. Вместо создание нового потока для нового запроса фреймворк может как-то заиспользовать уже имеющийся, который висит на ожидании данных. Но это всё туманно. Может есть benchmark'и какие-нибудь с циферками, где можно выйгрыш посмотреть?


  1. VanKrock
    10.07.2024 15:36

    Через стримы итак все умеют, лучше бы рассказали как это правильно через Pipe делать