Работа с файловой системой
- Большинство задач в программировании так или иначе связаны с работой с файлами и каталогами. Нам может потребоваться прочитать текст из файла или наоборот произвести запись, удалить файл или целый каталог, не говоря уже о более комплексных задачах, как например, создание текстового редактора и других подобных задачах.
- Фреймворк .NET предоставляет большие возможности по управлению и манипуляции файлами и каталогами, которые по большей части сосредоточены в пространстве имен System.IO. Классы, расположенные в этом пространстве имен (такие как Stream, StreamWriter, FileStream и др.), позволяют управлять файловым вводом-выводом.
Работа с дисками
- Работу с файловой системой начнем с самого верхнего уровня - дисков. Для представления диска в пространстве имен System.IO имеется класс DriveInfo.
Этот класс имеет статический метод GetDrives(), который возвращает имена всех логических дисков компьютера. Также он предоставляет ряд полезных свойств:
- AvailableFreeSpace: указывает на объем доступного свободного места на диске в байтах
- DriveFormat: получает имя файловой системы
- DriveType: представляет тип диска
- IsReady: готов ли диск (например, DVD-диск может быть не вставлен в дисковод)
- Name: получает имя диска
- RootDirectory: возвращает корневой каталог диска
- TotalFreeSpace: получает общий объем свободного места на диске в байтах
- TotalSize: общий размер диска в байтах
- VolumeLabel: получает или устанавливает метку тома
- Получим имена и свойства всех дисков на компьютере:
Консольный вывод:
Работа с каталогами
- Для работы с каталогами в пространстве имен System.IO предназначены сразу два класса: Directory и DirectoryInfo.
Класс Directory
Статический класс Directory предоставляет ряд методов для управления каталогами. Некоторые из этих методов:
- CreateDirectory(path): создает каталог по указанному пути path
- Delete(path): удаляет каталог по указанному пути path
- Exists(path): определяет, существует ли каталог по указанному пути path. Если существует, возвращается true, если не существует, то false
- GetCurrentDirectory(): получает путь к текущей папке
- GetDirectories(path): получает список подкаталогов в каталоге path
- GetFiles(path): получает список файлов в каталоге path
- GetFileSystemEntries(path): получает список подкаталогов и файлов в каталоге path
- Move(sourceDirName, destDirName): перемещает каталог
- GetParent(path): получение родительского каталога
- GetLastWriteTime(path): возвращает время последнего изменения каталога
- GetLastAccessTime(path): возвращает время последнего обращения к каталогу
- GetCreationTime(path): возвращает время создания каталога
Класс DirectoryInfo
- Данный класс предоставляет функциональность для создания, удаления, перемещения и других операций с каталогами. Во многом он похож на Directory, но не является статическим.
- Для создания объекта класса DirectoryInfo применяется конструктор, который в качестве параметра принимает путь к каталогу: 1 public DirectoryInfo (string path);
Основные методы класса DirectoryInfo:
- Create(): создает каталог
- CreateSubdirectory(path): создает подкаталог по указанному пути path
- Delete(): удаляет каталог
- GetDirectories(): получает список подкаталогов папки в виде массива DirectoryInfo
- GetFiles(): получает список файлов в папке в виде массива FileInfo
- MoveTo(destDirName): перемещает каталог
Основные свойства класса DirectoryInfo:
- CreationTime: представляет время создания каталога
- LastAccessTime: представляет время последнего доступа к каталогу
- LastWriteTime: представляет время последнего изменения каталога
- Exists: определяет, существует ли каталог
- Parent: получение родительского каталога
- Root: получение корневого каталога
- Name: имя каталога
- FullName: полный путь к каталогу
Directory или DirectoryInfo
- Как видно из функционала, оба класса предоставляют похожие возможности. Когда же и что использовать? Если надо совершить одну-две операции с одним каталогом, то проще использовать класс Directory. Если необходимо выполнить последовательность операций с одним и тем же каталогом, то лучше воспользоваться классом DirectoryInfo. Почему? Дело в том, что методы класса Directory выполняют дополнительные проверки безопасности. А для класса DirectoryInfo такие проверки не всегда обязательны.
Посмотрим на примерах применение этих классов
- Получение списка файлов и подкаталогов
Обратите внимание на использование слешей в именах файлов. Либо мы используем двойной слеш: "C:\\", либо одинарный, но тогда перед всем путем ставим знак @: @"C:\Program Files"
Аналогичный пример с DirectoryInfo:
Фильтрация папок и файлов
- Методы получения папок и файлов позволяют выполнять фильтрацию. В качестве фильтра в эти методы передается шаблон, который может содержать два плейсхолдера: * или символ-звездочка (соответствует любому количеству символов) и ? или вопросительный знак (соответствует одному символу)
- Например, найдем все папки, которые начинаются на "books":
- Или получим все файлы с расширением ".exe":
Создание каталога
Вначале проверяем, а нету ли такой директории, так как если она существует, то ее создать будет нельзя, и приложение выбросит ошибку. В итоге у нас получится следующий путь: "C:\SomeDir\program\avalon"
Аналогичный пример с классом Directory:
Получение информации о каталоге
Удаление каталога
- Если мы просто применим метод Delete к непустой папке, в которой есть какие-нибудь файлы или подкаталоги, то приложение нам выбросит ошибку. Поэтому нам надо передать в метод Delete дополнительный параметр булевого типа, который укажет, что папку надо удалять со всем содержимым. Кроме того, перед удалением следует проверить наличие удаляемой папки, иначе приложение выбросит исключение:
Или так:
Перемещение каталога
- При перемещении надо учитывать, что новый каталог, в который мы хотим перемесить все содержимое старого каталога, не должен существовать.
Перемещение каталога в рамках одной папки (как в примере выше) фактически аналогично переименованию папки
Работа с файлами. Классы File и FileInfo
- Подобно паре Directory/DirectoryInfo для работы с файлами предназначена пара классов File и FileInfo. С их помощью мы можем создавать, удалять, перемещать файлы, получать их свойства и многое другое.
Некоторые полезные методы и свойства класса FileInfo:
- CopyTo(path): копирует файл в новое место по указанному пути path
- Create(): создает файл
- Delete(): удаляет файл
- MoveTo(destFileName): перемещает файл в новое место
- Свойство Directory: получает родительский каталог в виде объекта DirectoryInfo
- Свойство DirectoryName: получает полный путь к родительскому каталогу
- Свойство Exists: указывает, существует ли файл
- Свойство Length: получает размер файла
- Свойство Extension: получает расширение файла
- Свойство Name: получает имя файла
- Свойство FullName: получает полное имя файла
- Класс File реализует похожую функциональность с помощью статических методов:
- Copy(): копирует файл в новое место
- Create(): создает файл
- Delete(): удаляет файл
- Move: перемещает файл в новое место
- Exists(file): определяет, существует ли файл
Получение информации о файле
Удаление файла
Перемещение файла
Копирование файла
- Метод CopyTo класса FileInfo принимает два параметра: путь, по которому файл будет копироваться, и булевое значение, которое указывает, надо ли при копировании перезаписывать файл (если true, как в случае выше, файл при копировании перезаписывается). Если же в качестве последнего параметра передать значение false, то если такой файл уже существует, приложение выдаст ошибку.
- Метод Copy класса File принимает три параметра: путь к исходному файлу, путь, по которому файл будет копироваться, и булевое значение, указывающее, будет ли файл перезаписываться.
FileStream. Чтение и запись файла
- Класс FileStream представляет возможности по считыванию из файла и записи в файл. Он позволяет работать как с текстовыми файлами, так и с бинарными.
Создание FileStream
- Для создания объекта FileStream можно использовать как конструкторы этого класса, так и статические методы класса File. Конструктор FileStream имеет множество перегруженных версий, из которых отмечу лишь одну, самую простую и используемую:
Здесь в конструктор передается два параметра: путь к файлу и перечисление FileMode. Данное перечисление указывает на режим доступа к файлу и может принимать следующие значения:
- Append: если файл существует, то текст добавляется в конец файл. Если файла нет, то он создается. Файл открывается только для записи.
- Create: создается новый файл. Если такой файл уже существует, то он перезаписывается
- CreateNew: создается новый файл. Если такой файл уже существует, то он приложение выбрасывает ошибку
- Open: открывает файл. Если файл не существует, выбрасывается исключение
- OpenOrCreate: если файл существует, он открывается, если нет - создается новый
- Truncate: если файл существует, то он перезаписывается. Файл открывается только для записи.
- Другой способ создания объекта FileStream представляют статические методы класса File:
- Первый метод открывает файл с учетом объекта FileMode и возвращает файловой поток FileStream. У этого метода также есть несколько перегруженных версий. Второй метод открывает поток для чтения, а третий открывает поток для записи.
Свойства и методы FileStream
Рассмотрим наиболее важные его свойства и методы класса FileStream:
- Свойство Length: возвращает длину потока в байтах
- Свойство Position: возвращает текущую позицию в потоке
- void CopyTo(Stream destination): копирует данные из текущего потока в поток destination
- Task CopyToAsync(Stream destination): асинхронная версия метода CopyToAsync
int Read(byte[] array, int offset, int count): считывает данные из файла в массив байтов и возвращает количество успешно считанных байтов. Принимает три параметра:
- array - массив байтов, куда будут помещены считываемые из файла данные
- offset представляет смещение в байтах в массиве array, в который считанные байты будут помещены
- count - максимальное число байтов, предназначенных для чтения. Если в файле находится меньшее количество байтов, то все они будут считаны.
- Task ReadAsync(byte[] array, int offset, int count): асинхронная версия метода Read
- long Seek(long offset, SeekOrigin origin): устанавливает позицию в потоке со смещением на количество байт, указанных в параметре offset.
void Write(byte[] array, int offset, int count): записывает в файл данные из массива байтов. Принимает три параметра:
- array - массив байтов, откуда данные будут записываться в файл
- offset - смещение в байтах в массиве array, откуда начинается запись байтов в поток
- count - максимальное число байтов, предназначенных для записи
- ValueTask WriteAsync(byte[] array, int offset, int count): асинхронная версия метода Write
Чтение и запись файлов
- FileStream представляет доступ к файлам на уровне байтов, поэтому, например, если вам надо считать или записать одну или несколько строк в текстовый файл, то массив байтов надо преобразовать в строки, используя специальные методы. Поэтому для работы с текстовыми файлами применяются другие классы.
- В то же время при работе с различными бинарными файлами, имеющими определенную структуру, FileStream может быть очень даже полезен для извлечения определенных порций информации и ее обработки.
Посмотрим на примере считывания-записи в текстовый файл:
- Разберем этот пример. Вначале создается папка для файла. Кроме того, на уровне операционной системы могут быть установлены ограничения на запись в определенных каталогах, и при попытке создания и записи файла в подобных каталогах мы получим ошибку.
- И при чтении, и при записи используется оператор using. Не надо путать данный оператор с директивой using, которая подключает пространства имен в начале файла кода. Оператор using позволяет создавать объект в блоке кода, по завершению которого вызывается метод Dispose у этого объекта, и, таким образом, объект уничтожается. В данном случае в качестве такого объекта служит переменная fstream.
- И при записи, и при чтении применяется объект кодировки Encoding.Default из пространства имен System.Text. В данном случае мы используем два его метода: GetBytes для получения массива байтов из строки и GetString для получения строки из массива байтов.
- В итоге введенная нами строка записывается в файл note.txt. По сути это бинарный файл (не текстовый), хотя если мы в него запишем только строку, то сможем посмотреть в удобочитаемом виде этот файл, открыв его в текстовом редакторе. Однако если мы в него запишем случайные байты, например:
- То у нас могут возникнуть проблемы с его пониманием. Поэтому для работы непосредственно с текстовыми файлами предназначены отдельные классы - StreamReader и StreamWriter.
- Хотя в данном простеньком консольном приложении, но в реальных приложениях рекомендуется использовать асинхронные версии методов FileStream, поскольку операции с файлами могут занимать продолжительное время и являются узким местом в работе программы. Например, изменим выше приведенную программу, применив асинхронные методы:
Произвольный доступ к файлам
- Нередко бинарные файлы представляют определенную структуру. И, зная эту структуру, мы можем взять из файла нужную порцию информации или наоборот записать в определенном месте файла определенный набор байтов. Например, в wav-файлах непосредственно звуковые данные начинаются с 44 байта, а до 44 байта идут различные метаданные - количество каналов аудио, частота дискретизации и т.д.
С помощью метода Seek() мы можем управлять положением курсора потока, начиная с которого производится считывание или запись в файл. Этот метод принимает два параметра: offset (смещение) и позиция в файле. Позиция в файле описывается тремя значениями:
- SeekOrigin.Begin: начало файла
- SeekOrigin.End: конец файла
- SeekOrigin.Current: текущая позиция в файле
- Курсор потока, с которого начинается чтение или запись, смещается вперед на значение offset относительно позиции, указанной в качестве второго параметра. Смещение может быть отрицательным, тогда курсор сдвигается назад, если положительное - то вперед.
Рассмотрим на примере:
Консольный вывод:
- Вызов fstream.Seek(-5, SeekOrigin.End) перемещает курсор потока в конец файлов назад на пять символов:
То есть после записи в новый файл строки "hello world" курсор будет стоять на позиции символа "w". После этого считываем четыре байта начиная с символа "w". В данной кодировке 1 символ будет представлять 1 байт. Поэтому чтение 4 байтов будет эквивалентно чтению четырех сиволов: "worl". Затем опять же перемещаемся в конец файла, не доходя до конца пять символов (то есть опять же с позиции символа "w"), и осуществляем запись строки "house". Таким образом, строка "house" заменяет строку "world".
Закрытие потока
- В примерах выше для закрытия потока применяется конструкция using. После того как все операторы и выражения в блоке using отработают, объект FileStream уничтожается. Однако мы можем выбрать и другой способ:
- Если мы не используем конструкцию using, то нам надо явным образом вызвать метод Close(): fstream.Close()
Чтение и запись текстовых файлов. StreamReader и StreamWriter
- Класс FileStream не очень удобно применять для работы с текстовыми файлами. К тому же для этого в пространстве System.IO определены специальные классы: StreamReader и StreamWriter.
Запись в файл и StreamWriter
Для записи в текстовый файл используется класс StreamWriter. Некоторые из его конструкторов, которые могут применяться для создания объекта StreamWriter:
- StreamWriter(string path): через параметр path передается путь к файлу, который будет связан с потоком
- StreamWriter(string path, bool append): параметр append указывает, надо ли добавлять в конец файла данные или же перезаписывать файл. Если равно true, то новые данные добавляются в конец файла. Если равно false, то файл перезаписываетсяя заново
- StreamWriter(string path, bool append, System.Text.Encoding encoding): параметр encoding указывает на кодировку, которая будет применяться при записи
Свою функциональность StreamWriter реализует через следующие методы:
- int Close(): закрывает записываемый файл и освобождает все ресурсы
- void Flush(): записывает в файл оставшиеся в буфере данные и очищает буфер.
- Task FlushAsync(): асинхронная версия метода Flush
- void Write(string value): записывает в файл данные простейших типов, как int, double, char, string и т.д. Соответственно имеет ряд перегруженных версий для записи данных элементарных типов, например, Write(char value), Write(int value), Write(double value) и т.д.
- Task WriteAsync(string value): асинхронная версия метода Write
- void WriteLine(string value): также записывает данные, только после записи добавляет в файл символ окончания строки
- Task WriteLineAsync(string value): асинхронная версия метода WriteLine
Рассмотрим запись в файл на примере:
- В данном случае два раза создаем объект StreamWriter. В первом случае если файл существует, то он будет перезаписан. Если не существует, он будет создан. И в нее будет записан текст из переменной text. Во втором случае файл открывается для дозаписи, и будут записаны атомарные данные - строка и число. В обоих случаях будет использоваться кодировка по умолчанию.
- По завершении программы в папке C://SomeDir мы сможем найти файл hta.txt, который будет иметь следующие строки:
- Поскольку операции с файлами могут занимать продолжительное время, то в общем случае рекомендуется использовать асинхронную запись. Используем асинхронные версии методов:
Обратите внимание, что асинхронные версии есть не для всех перегрузок метода Write.
Чтение из файла и StreamReader
- Класс StreamReader позволяет нам легко считывать весь текст или отдельные строки из текстового файла.
Некоторые из конструкторов класса StreamReader:
- StreamReader(string path): через параметр path передается путь к считываемому файлу
- StreamReader(string path, System.Text.Encoding encoding): параметр encoding задает кодировку для чтения файла
Среди методов StreamReader можно выделить следующие:
- void Close(): закрывает считываемый файл и освобождает все ресурсы
- int Peek(): возвращает следующий доступный символ, если символов больше нет, то возвращает -1
- int Read(): считывает и возвращает следующий символ в численном представлении. Имеет перегруженную версию: Read(char[] array, int index, int count), где array - массив, куда считываются символы, index - индекс в массиве array, начиная с которого записываются считываемые символы, и count - максимальное количество считываемых символов
- Task ReadAsync(): асинхронная версия метода Read
- string ReadLine(): считывает одну строку в файле
- string ReadLineAsync(): асинхронная версия метода ReadLine
- string ReadToEnd(): считывает весь текст из файла
- string ReadToEndAsync(): асинхронная версия метода ReadToEnd
Сначала считаем текст полностью из ранее записанного файла:
Считаем текст из файла построчно:
- В данном случае считываем построчно через цикл while: while ((line = sr.ReadLine()) != null) - сначала присваиваем переменной line результат функции sr.ReadLine(), а затем проверяем, не равна ли она null. Когда объект sr дойдет до конца файла и больше строк не останется, то метод sr.ReadLine() будет возвращать null.