Предыдущая лекция | Следующая лекция | |
---|---|---|
Многопоточность. Потоки, асинхронные вычисления | Содержание | Типы файлов: CSV, XML, JSON. |
Большинство задач в программировании так или иначе связаны с работой с файлами и каталогами. Нам может потребоваться прочитать текст из файла или наоборот произвести запись, удалить файл или целый каталог, не говоря уже о более комплексных задачах, как например, создание текстового редактора и других подобных задачах.
Фреймворк .NET предоставляет большие возможности по управлению и манипуляции файлами и каталогами, которые по большей части сосредоточены в пространстве имен System.IO
. Классы, расположенные в этом пространстве имен (такие как Stream, StreamWriter, FileStream и др.), позволяют управлять файловым вводом-выводом.
Работу с файловой системой начнем с самого верхнего уровня - дисков. Для представления диска в пространстве имен System.IO
имеется класс DriveInfo.
Этот класс имеет статический метод GetDrives, который возвращает имена всех логических дисков компьютера. Также он предоставляет ряд полезных свойств:
Получим имена и свойства всех дисков на компьютере:
using System;
using System.IO;
DriveInfo[] drives = DriveInfo.GetDrives();
foreach (DriveInfo drive in drives)
{
Console.WriteLine($"Название: {drive.Name}");
Console.WriteLine($"Тип: {drive.DriveType}");
if (drive.IsReady)
{
Console.WriteLine($"Объем диска: {drive.TotalSize}");
Console.WriteLine($"Свободное пространство: {drive.TotalFreeSpace}");
Console.WriteLine($"Метка: {drive.VolumeLabel}");
}
Console.WriteLine();
}
У меня на компьютере Linux, поэтому дисков оказалось очень много, под Windows скорее всего будет один
/home/kei/RiderProjects/OAP/stream/bin/Debug/net8.0/stream
Название: /proc
Тип: Ram
Объем диска: 0
Свободное пространство: 0
Метка: /proc
Название: /sys
Тип: Ram
Объем диска: 0
Свободное пространство: 0
Метка: /sys
Название: /dev
Тип: Ram
Объем диска: 8236191744
Свободное пространство: 8236191744
Метка: /dev
Название: /run
Тип: Ram
Объем диска: 8254484480
Свободное пространство: 8252055552
Метка: /run
Название: /sys/firmware/efi/efivars
Тип: Unknown
Объем диска: 188328
Свободное пространство: 107141
Метка: /sys/firmware/efi/efivars
Название: /dev/pts
Тип: Ram
Объем диска: 0
Свободное пространство: 0
Метка: /dev/pts
Название: /
Тип: Fixed
Объем диска: 1006662447104
Свободное пространство: 862912659456
Метка: /
Название: /sys/kernel/security
Тип: Ram
Объем диска: 0
Свободное пространство: 0
Метка: /sys/kernel/security
Название: /dev/shm
Тип: Ram
Объем диска: 8254484480
Свободное пространство: 8211234816
Метка: /dev/shm
Название: /sys/fs/cgroup
Тип: Ram
Объем диска: 0
Свободное пространство: 0
Метка: /sys/fs/cgroup
Название: /sys/fs/pstore
Тип: Fixed
Объем диска: 0
Свободное пространство: 0
Метка: /sys/fs/pstore
Название: /sys/fs/bpf
Тип: Fixed
Объем диска: 0
Свободное пространство: 0
Метка: /sys/fs/bpf
Название: /proc/sys/fs/binfmt_misc
Тип: Ram
Объем диска: 0
Свободное пространство: 0
Метка: /proc/sys/fs/binfmt_misc
Название: /dev/mqueue
Тип: Ram
Объем диска: 0
Свободное пространство: 0
Метка: /dev/mqueue
Название: /dev/hugepages
Тип: Ram
Объем диска: 0
Свободное пространство: 0
Метка: /dev/hugepages
Название: /sys/kernel/debug
Тип: Ram
Объем диска: 0
Свободное пространство: 0
Метка: /sys/kernel/debug
Название: /sys/kernel/tracing
Тип: Fixed
Объем диска: 0
Свободное пространство: 0
Метка: /sys/kernel/tracing
Название: /sys/fs/fuse/connections
Тип: Ram
Объем диска: 0
Свободное пространство: 0
Метка: /sys/fs/fuse/connections
Название: /sys/kernel/config
Тип: Ram
Объем диска: 0
Свободное пространство: 0
Метка: /sys/kernel/config
Название: /tmp
Тип: Ram
Объем диска: 8254484480
Свободное пространство: 8238710784
Метка: /tmp
Название: /boot/efi
Тип: Fixed
Объем диска: 313942016
Свободное пространство: 302096384
Метка: /boot/efi
Название: /var/lib/docker/overlay2/4a2802316a49f1a30573524d0059ce49f6cd6cfe40608b77efb6febce3b2d66d/merged
Тип: Unknown
Название: /var/lib/docker/overlay2/a91156ea1396c9882176076645afbc6b4c5ded5492d9226442a6c5b55d07fd67/merged
Тип: Unknown
Название: /var/lib/docker/overlay2/de7b5cae3507be9b60ff7a3ddb8375e6968f465e3d926eb1c56c0206c8e2e17a/merged
Тип: Unknown
Название: /var/lib/docker/overlay2/f6ab6ef9b79dabd97d12c898d4239d1fb173ebcd83a1abf5c22704e730390115/merged
Тип: Unknown
Название: /var/lib/docker/overlay2/cf4ba0731412147587fe830cbcc73005b3f660443a73da88977222e55c304e27/merged
Тип: Unknown
Название: /run/docker/netns/default
Тип: Unknown
Название: /run/docker/netns/fe8e9119c528
Тип: Unknown
Название: /run/user/1000
Тип: Ram
Объем диска: 1650896896
Свободное пространство: 1650798592
Метка: /run/user/1000
Название: /run/user/1000/doc
Тип: Unknown
Unhandled exception. System.UnauthorizedAccessException: Access to the path is denied.
---> System.IO.IOException: Operation not permitted
--- End of inner exception stack trace ---
at System.IO.DriveInfo.CheckStatfsResultAndThrowIfNecessary(Int32 result)
at System.IO.DriveInfo.get_TotalSize()
at Program.<Main>$(String[] args) in /home/kei/RiderProjects/OAP/stream/Program.cs:line 9
Process finished with exit code 134.
Для работы с каталогами в пространстве имен System.IO
предназначены сразу два класса: Directory и DirectoryInfo.
Класс Directory предоставляет ряд статических методов для управления каталогами. Некоторые из этих методов:
Данный класс предоставляет функциональность для создания, удаления, перемещения и других операций с каталогами. Во многом он похож на Directory. Некоторые из его свойств и методов:
Посмотрим на примерах применение этих классов
string dirName = "C:\\";
if (Directory.Exists(dirName))
{
Console.WriteLine("Подкаталоги:");
string[] dirs = Directory.GetDirectories(dirName);
foreach (string s in dirs)
{
Console.WriteLine(s);
}
Console.WriteLine();
Console.WriteLine("Файлы:");
string[] files = Directory.GetFiles(dirName);
foreach (string s in files)
{
Console.WriteLine(s);
}
}
Обратите внимание на использование слешей в именах файлов. Либо мы используем двойной слеш: C:\\
, либо одинарный, но тогда перед строкой ставим знак @
: @"C:\Program Files"
Можно вместо обратных слешей использовать прямые. Windows нормально их воспринимает
string path = @"C:\SomeDir";
string subpath = @"program\avalon";
DirectoryInfo dirInfo = new DirectoryInfo(path);
if (!dirInfo.Exists)
{
dirInfo.Create();
}
dirInfo.CreateSubdirectory(subpath);
Вначале проверяем, а нет ли такой директории, так как если она существует, то ее создать будет нельзя, и приложение выбросит ошибку. В итоге у нас получится следующий путь: C:\SomeDir\program\avalon
string dirName = "C:\\Program Files";
DirectoryInfo dirInfo = new DirectoryInfo(dirName);
Console.WriteLine($"Название каталога: {dirInfo.Name}");
Console.WriteLine($"Полное название каталога: {dirInfo.FullName}");
Console.WriteLine($"Время создания каталога: {dirInfo.CreationTime}");
Console.WriteLine($"Корневой каталог: {dirInfo.Root}");
Если мы просто применим метод Delete к непустой папке, в которой есть какие-нибудь файлы или подкаталоги, то приложение нам выбросит ошибку. Поэтому нам надо передать в метод Delete дополнительный параметр булевого типа, который укажет, что папку надо удалять со всем содержимым:
string dirName = @"C:\SomeFolder";
try
{
DirectoryInfo dirInfo = new DirectoryInfo(dirName);
dirInfo.Delete(true);
Console.WriteLine("Каталог удален");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Или так:
string dirName = @"C:\SomeFolder";
Directory.Delete(dirName, true);
string oldPath = @"C:\SomeFolder";
string newPath = @"C:\SomeDir";
DirectoryInfo dirInfo = new DirectoryInfo(oldPath);
if (dirInfo.Exists && Directory.Exists(newPath) == false)
{
dirInfo.MoveTo(newPath);
}
При перемещении надо учитывать, что новый каталог, в который мы хотим перемесить все содержимое старого каталога, не должен существовать.
Подобно паре Directory/DirectoryInfo для работы с файлами предназначена пара классов File и FileInfo. С их помощью мы можем создавать, удалять, перемещать файлы, получать их свойства и многое другое.
Некоторые полезные методы и свойства класса FileInfo:
Класс File реализует похожую функциональность с помощью статических методов:
string path = @"C:\apache\hta.txt";
FileInfo fileInf = new FileInfo(path);
if (fileInf.Exists)
{
Console.WriteLine("Имя файла: {0}", fileInf.Name);
Console.WriteLine("Время создания: {0}", fileInf.CreationTime);
Console.WriteLine("Размер: {0}", fileInf.Length);
}
string path = @"C:\apache\hta.txt";
FileInfo fileInf = new FileInfo(path);
if (fileInf.Exists)
{
fileInf.Delete();
// альтернатива с помощью класса File
// File.Delete(path);
}
string path = @"C:\apache\hta.txt";
string newPath = @"C:\SomeDir\hta.txt";
FileInfo fileInf = new FileInfo(path);
if (fileInf.Exists)
{
fileInf.MoveTo(newPath);
// альтернатива с помощью класса File
// File.Move(path, newPath);
}
string path = @"C:\apache\hta.txt";
string newPath = @"C:\SomeDir\hta.txt";
FileInfo fileInf = new FileInfo(path);
if (fileInf.Exists)
{
fileInf.CopyTo(newPath, true);
// альтернатива с помощью класса File
// File.Copy(path, newPath, true);
}
Метод CopyTo класса FileInfo принимает два параметра: путь, по которому файл будет копироваться, и булевое значение, которое указывает, надо ли при копировании перезаписывать файл (если true, как в случае выше, файл при копировании перезаписывается). Если же в качестве последнего параметра передать значение false, то если такой файл уже существует, приложение выдаст ошибку.
Метод Copy класса File принимает три параметра: путь к исходному файлу, путь, по которому файл будет копироваться, и булевое значение, указывающее, будет ли файл перезаписываться.
Класс FileStream представляет возможности по считыванию из файла и записи в файл. Он позволяет работать как с текстовыми файлами, так и с бинарными.
Для создания объекта FileStream можно использовать как конструкторы этого класса, так и статические методы класса File. Конструктор FileStream имеет множество перегруженных версий, из которых отмечу лишь одну, самую простую и используемую:
FileStream(string filename, FileMode mode)
Здесь в конструктор передается два параметра: путь к файлу и перечисление (enum) FileMode. Данное перечисление указывает на режим доступа к файлу и может принимать следующие значения:
Другой способ создания объекта FileStream представляют статические методы класса File:
FileStream File.Open(string file, FileMode mode);
FileStream File.OpenRead(string file);
FileStream File.OpenWrite(string file);
Первый метод открывает файл с учетом объекта FileMode и возвращает файловой поток FileStream. У этого метода также есть несколько перегруженных версий. Второй метод открывает поток для чтения, а третий открывает поток для записи.
Рассмотрим наиболее важные его свойства и методы класса FileStream:
int Read(byte[] array, int offset, int count): считывает данные из файла в массив байтов и возвращает количество успешно считанных байтов. Принимает три параметра:
array - массив байтов, куда будут помещены считываемые из файла данные
offset представляет смещение в байтах в массиве array, в который считанные байты будут помещены
count - максимальное число байтов, предназначенных для чтения. Если в файле находится меньшее количество байтов, то все они будут считаны.
Task<int> 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 System;
using System.IO;
namespace HelloApp
{
class Program
{
static void Main(string[] args)
{
// создаем каталог для файла
string path = @"C:\SomeDir2";
DirectoryInfo dirInfo = new DirectoryInfo(path);
if (!dirInfo.Exists)
{
dirInfo.Create();
}
Console.WriteLine("Введите строку для записи в файл:");
string text = Console.ReadLine();
// запись в файл
using (FileStream fstream = new FileStream($"{path}\note.txt", FileMode.OpenOrCreate))
{
// преобразуем строку в байты
byte[] array = System.Text.Encoding.Default.GetBytes(text);
// запись массива байтов в файл
fstream.Write(array, 0, array.Length);
Console.WriteLine("Текст записан в файл");
}
// чтение из файла
using (FileStream fstream = File.OpenRead($"{path}\note.txt"))
{
// преобразуем строку в байты
byte[] array = new byte[fstream.Length];
// считываем данные
fstream.Read(array, 0, array.Length);
// декодируем байты в строку
string textFromFile = System.Text.Encoding.Default.GetString(array);
Console.WriteLine($"Текст из файла: {textFromFile}");
}
Console.ReadLine();
}
}
}
Разберем этот пример. Вначале создается папка для файла. Кроме того, на уровне операционной системы могут быть установлены ограничения на запись в определенных каталогах, и при попытке создания и записи файла в подобных каталогах мы получим ошибку.
И при чтении, и при записи используется оператор using. Не надо путать данный оператор с директивой using, которая подключает пространства имен в начале файла кода. Оператор using позволяет создавать объект в блоке кода, по завершению которого вызывается метод Dispose у этого объекта, и, таким образом, объект уничтожается. В данном случае в качестве такого объекта служит переменная fstream.
И при записи, и при чтении применяется объект кодировки Encoding.Default
из пространства имен System.Text
. В данном случае мы используем два его метода: GetBytes для получения массива байтов из строки и GetString для получения строки из массива байтов.
В итоге введенная нами строка записывается в файл note.txt
. По сути это бинарный файл (не текстовый), хотя если мы в него запишем только строку, то сможем посмотреть в удобочитаемом виде этот файл, открыв его в текстовом редакторе. Однако если мы в него запишем случайные байты, например:
fstream.WriteByte(13);
fstream.WriteByte(103);
То у нас могут возникнуть проблемы с его пониманием. Поэтому для работы непосредственно с текстовыми файлами предназначены отдельные классы - StreamReader и StreamWriter.
В реальных приложениях рекомендуется использовать асинхронные версии методов FileStream, поскольку операции с файлами могут занимать продолжительное время и являются узким местом в работе программы. Например, изменим выше приведенную программу, применив асинхронные методы:
using System;
using System.IO;
using System.Threading.Tasks;
namespace HelloApp
{
class Program
{
static async Task Main(string[] args)
{
// создаем каталог для файла
string path = @"C:\SomeDir3";
DirectoryInfo dirInfo = new DirectoryInfo(path);
if (!dirInfo.Exists)
{
dirInfo.Create();
}
Console.WriteLine("Введите строку для записи в файл:");
string text = Console.ReadLine();
// запись в файл
using (FileStream fstream = new FileStream($"{path}\note.txt", FileMode.OpenOrCreate))
{
byte[] array = System.Text.Encoding.Default.GetBytes(text);
// асинхронная запись массива байтов в файл
await fstream.WriteAsync(array, 0, array.Length);
Console.WriteLine("Текст записан в файл");
}
// чтение из файла
using (FileStream fstream = File.OpenRead($"{path}\note.txt"))
{
byte[] array = new byte[fstream.Length];
// асинхронное чтение файла
await fstream.ReadAsync(array, 0, array.Length);
string textFromFile = System.Text.Encoding.Default.GetString(array);
Console.WriteLine($"Текст из файла: {textFromFile}");
}
Console.ReadLine();
}
}
}
Нередко бинарные файлы представляют определенную структуру. И, зная эту структуру, мы можем взять из файла нужную порцию информации или наоброт записать в определенном месте файла определенный набор байтов. Например, в wav-файлах непосредственно звуковые данные начинаются с 44 байта, а до 44 байта идут различные метаданные - количество каналов аудио, частота дискретизации и т.д.
С помощью метода Seek мы можем управлять положением курсора потока, начиная с которого производится считывание или запись в файл. Этот метод принимает два параметра: offset (смещение) и позиция в файле. Позиция в файле описывается тремя значениями:
SeekOrigin.Begin
: начало файлаSeekOrigin.End
: конец файлаSeekOrigin.Current
: текущая позиция в файлеКурсор потока, с которого начинается чтение или запись, смещается вперед на значение offset относительно позиции, указанной в качестве второго параметра. Смещение может быть отрицательным, тогда курсор сдвигается назад, если положительное - то вперед.
Рассмотрим на примере:
using System.IO;
using System.Text;
class Program
{
static void Main(string[] args)
{
string text = "hello world";
// запись в файл
using (FileStream fstream = new FileStream(@"D:\note.dat", FileMode.OpenOrCreate))
{
// преобразуем строку в байты
byte[] input = Encoding.Default.GetBytes(text);
// запись массива байтов в файл
fstream.Write(input, 0, input.Length);
Console.WriteLine("Текст записан в файл");
// перемещаем указатель в конец файла, до конца файла- пять байт
fstream.Seek(-5, SeekOrigin.End); // минус 5 символов с конца потока
// считываем четыре символов с текущей позиции
byte[] output = new byte[4];
fstream.Read(output, 0, output.Length);
// декодируем байты в строку
string textFromFile = Encoding.Default.GetString(output);
Console.WriteLine($"Текст из файла: {textFromFile}"); // worl
// заменим в файле слово world на слово house
string replaceText = "house";
fstream.Seek(-5, SeekOrigin.End); // минус 5 символов с конца потока
input = Encoding.Default.GetBytes(replaceText);
fstream.Write(input, 0, input.Length);
// считываем весь файл
// возвращаем указатель в начало файла
fstream.Seek(0, SeekOrigin.Begin);
output = new byte[fstream.Length];
fstream.Read(output, 0, output.Length);
// декодируем байты в строку
textFromFile = Encoding.Default.GetString(output);
Console.WriteLine($"Текст из файла: {textFromFile}"); // hello house
}
Console.Read();
}
}
Консольный вывод:
Текст записан в файл
Текст из файла: worl
Текст из файла: hello house
Вызов fstream.Seek(-5, SeekOrigin.End)
перемещает курсор потока в конец файлов назад на пять символов:
То есть после записи в новый файл строки "hello world" курсор будет стоять на позиции символа "w".
После этого считываем четыре байта начиная с символа "w". В данной кодировке один символ будет представлять один байт. Поэтому чтение 4-х байтов будет эквивалентно чтению четырех сиволов: "worl".
Затем опять же перемещаемся в конец файла, не доходя до конца пять символов (то есть опять же с позиции символа "w"), и осуществляем запись строки "house". Таким образом, строка "house" заменяет строку "world".
В примерах выше для закрытия потока применяется конструкция using. После того как все операторы и выражения в блоке using отработают, объект FileStream уничтожается. Однако мы можем выбрать и другой способ:
FileStream fstream = null;
try
{
fstream = new FileStream(@"D:\note3.dat", FileMode.OpenOrCreate);
// операции с потоком
}
catch(Exception ex)
{
}
finally
{
if (fstream != null)
fstream.Close();
}
Если мы не используем конструкцию using, то нам надо явным образом вызвать метод Close: fstream.Close()
Класс FileStream не очень удобно применять для работы с текстовыми файлами. Для этого в пространстве System.IO
определены специальные классы: StreamReader и 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
Рассмотрим запись в файл на примере:
using System;
using System.IO;
namespace HelloApp
{
class Program
{
static void Main(string[] args)
{
string writePath = @"C:\SomeDir\hta.txt";
string text = "Привет мир!\nПока мир...";
try
{
using (StreamWriter sw = new StreamWriter(
writePath, false, System.Text.Encoding.Default))
{
sw.WriteLine(text);
}
using (StreamWriter sw = new StreamWriter(
writePath, true, System.Text.Encoding.Default))
{
sw.WriteLine("Дозапись");
sw.Write(4.5);
}
Console.WriteLine("Запись выполнена");
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
}
}
В данном случае два раза создаем объект StreamWriter. В первом случае если файл существует, то он будет перезаписан. Если не существует, он будет создан. И в нее будет записан текст из переменной text. Во втором случае файл открывается для дозаписи, и будут записаны атомарные данные - строка и число. В обоих случаях будет использоваться кодировка по умолчанию.
По завершении программы в папке C:/SomeDir
мы сможем найти файл hta.txt
, который будет иметь следующие строки:
Привет мир!
Пока мир...
Дозапись
4,5
Поскольку операции с файлами могут занимать продолжительное время, то в общем случае рекомендуется использовать асинхронную запись. Используем асинхронные версии методов:
using System;
using System.IO;
using System.Threading.Tasks;
namespace HelloApp
{
class Program
{
static async Task Main(string[] args)
{
string writePath = @"C:\SomeDir\hta2.txt";
string text = "Привет мир!\nПока мир...";
try
{
using (StreamWriter sw = new StreamWriter(
writePath, false, System.Text.Encoding.Default))
{
await sw.WriteLineAsync(text);
}
using (StreamWriter sw = new StreamWriter(
writePath, true, System.Text.Encoding.Default))
{
await sw.WriteLineAsync("Дозапись");
await sw.WriteAsync("4,5");
}
Console.WriteLine("Запись выполнена");
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
}
}
Обратите внимание, что асинхронные версии есть не для всех перегрузок метода Write.
Класс 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<int> ReadAsync()
: асинхронная версия метода Read
string ReadLine()
: считывает одну строку в файле
string ReadLineAsync()
: асинхронная версия метода ReadLine
string ReadToEnd()
: считывает весь текст из файла
string ReadToEndAsync()
: асинхронная версия метода ReadToEnd
Сначала считаем текст полностью из ранее записанного файла:
using System;
using System.IO;
using System.Threading.Tasks;
string path = @"C:\SomeDir\hta.txt";
try
{
using (StreamReader sr = new StreamReader(path))
{
Console.WriteLine(sr.ReadToEnd());
}
// асинхронное чтение
using (StreamReader sr = new StreamReader(path))
{
Console.WriteLine(await sr.ReadToEndAsync());
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
Считаем текст из файла построчно:
string path= @"C:\SomeDir\hta.txt";
using (StreamReader sr = new StreamReader(path, System.Text.Encoding.Default))
{
string line;
while ((line = sr.ReadLine()) != null)
{
Console.WriteLine(line);
}
}
// асинхронное чтение
using (StreamReader sr = new StreamReader(path, System.Text.Encoding.Default))
{
string line;
while ((line = await sr.ReadLineAsync()) != null)
{
Console.WriteLine(line);
}
}
В данном случае считываем построчно через цикл while: while ((line = sr.ReadLine()) != null)
- сначала присваиваем переменной line результат функции sr.ReadLine()
, а затем проверяем, не равна ли она null. Когда объект sr
дойдет до конца файла и больше строк не останется, то метод sr.ReadLine()
будет возвращать null.
Для работы с бинарными файлами предназначена пара классов BinaryWriter и BinaryReader. Эти классы позволяют читать и записывать данные в двоичном формате.
Основные метода класса BinaryWriter
Close()
: закрывает поток и освобождает ресурсы
Flush()
: очищает буфер, дописывая из него оставшиеся данные в файл
Seek()
: устанавливает позицию в потоке
Write()
: записывает данные в поток
Основные метода класса BinaryReader
Close()
: закрывает поток и освобождает ресурсы
ReadBoolean()
: считывает значение bool и перемещает указатель на один байт
ReadByte(): считывает один байт и перемещает указатель на один байт
ReadChar()
: считывает значение char, то есть один символ, и перемещает указатель на столько байтов, сколько занимает символ в текущей кодировке
ReadDecimal()
: считывает значение decimal и перемещает указатель на 16 байт
ReadDouble()
: считывает значение double и перемещает указатель на 8 байт
ReadInt16()
: считывает значение short и перемещает указатель на 2 байта
ReadInt32()
: считывает значение int и перемещает указатель на 4 байта
ReadInt64()
: считывает значение long и перемещает указатель на 8 байт
ReadSingle()
: считывает значение float и перемещает указатель на 4 байта
ReadString()
: считывает значение string. Каждая строка предваряется значением длины строки, которое представляет 7-битное целое число
С чтением бинарных данных все просто: соответствующий метод считывает данные определенного типа и перемещает указатель на размер этого типа в байтах, например, значение типа int занимает 4 байта, поэтому BinaryReader считает 4 байта и переместит указать на эти 4 байта.
Посмотрим на реальной задаче применение этих классов. Попробуем с их помощью записывать и считывать из файла массив структур:
struct State
{
public string name;
public string capital;
public int area;
public double people;
public State(string n, string c, int a, double p)
{
name = n;
capital = c;
people = p;
area = a;
}
}
class Program
{
static void Main(string[] args)
{
State[] states = new State[2];
states[0] = new State("Германия", "Берлин", 357168, 80.8);
states[1] = new State("Франция", "Париж", 640679, 64.7);
string path= @"C:\SomeDir\states.dat";
try
{
// создаем объект BinaryWriter
using (BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.OpenOrCreate)))
{
// записываем в файл значение каждого поля структуры
foreach (State s in states)
{
writer.Write(s.name);
writer.Write(s.capital);
writer.Write(s.area);
writer.Write(s.people);
}
}
// создаем объект BinaryReader
using (BinaryReader reader = new BinaryReader(File.Open(path, FileMode.Open)))
{
// пока не достигнут конец файла
// считываем каждое значение из файла
while (reader.PeekChar() > -1)
{
string name = reader.ReadString();
string capital = reader.ReadString();
int area = reader.ReadInt32();
double population = reader.ReadDouble();
Console.WriteLine("Страна: {0} столица: {1} площадь {2} кв. км численность населения: {3} млн. чел.",
name, capital, area, population);
}
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
Console.ReadLine();
}
}
Итак, у нас есть структура State с некоторым набором полей. В основной программе создаем массив структур и записываем с помощью BinaryWriter. Этот класс в качестве параметра в конструкторе принимает объект Stream, который создается вызовом File.Open(path, FileMode.OpenOrCreate)
.
Затем в цикле пробегаемся по массиву структур и записываем каждое поле структуры в поток. В том порядке, в каком эти значения полей записываются, в том порядке они и будут размещаться в файле.
Затем считываем из записанного файла. Конструктор класса BinaryReader также в качестве параметра принимает объект потока, только в данном случае устанавливаем в качестве режима FileMode.Open: new BinaryReader(File.Open(path, FileMode.Open))
В цикле while считываем данные. Чтобы узнать окончание потока, вызываем метод PeekChar. Этот метод считывает следующий символ и возвращает его числовое представление. Если символ отсутствует, то метод возвращает -1
, что будет означать, что мы достигли конца файла.
В цикле последовательно считываем значения поле структур в том же порядке, в каком они записывались.
Таким образом, классы BinaryWriter и BinaryReader очень удобны для работы с бинарными файлами, особенно когда нам известна структура этих файлов. В то же время для хранения и считывания более комплексных объектов, например, объектов классов, лучше подходит другое решение - сериализация.
В прошлых темах было рассмотрено как сохранять и считывать информацию с текстовых и бинарных файлов с помощью классов из пространства System.IO
. Но .NET также предоставляет еще один механизм для удобной работы с бинарными файлами и их данными - бинарную сериализацию. Сериализация представляет процесс преобразования какого-либо объекта в поток байтов. После преобразования мы можем этот поток байтов или записать на диск или сохранить его временно в памяти. А при необходимости можно выполнить обратный процесс - десериализацию, то есть получить из потока байтов ранее сохраненный объект.
Чтобы объект определенного класса можно было сериализовать, надо этот класс пометить атрибутом Serializable:
[Serializable]
class Person
{
public string Name { get; set; }
public int Year { get; set; }
public Person(string name, int year)
{
Name = name;
Year = year;
}
}
При отстутствии данного атрибута объект Person не сможет быть сериализован, и при попытке сериализации будет выброшено исключение SerializationException.
Сериализация применяется к свойствам и полям класса. Если мы не хотим, чтобы какое-то поле класса сериализовалось, то мы его помечаем атрибутом NonSerialized:
[Serializable]
class Person
{
public string Name { get; set; }
public int Year { get; set; }
[NonSerialized]
public string accNumber;
public Person(string name, int year, string acc)
{
Name = name;
Year = year;
accNumber = acc;
}
}
При наследовании подобного класса, следует учитывать, что атрибут Serializable автоматически не наследуется. И если мы хотим, чтобы производный класс также мог бы быть сериализован, то опять же мы применяем к нему атрибут:
[Serializable]
class Worker : Person
Для бинарной сериализации применяется класс BinaryFormatter:
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
namespace Serialization
{
[Serializable]
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
class Program
{
static void Main(string[] args)
{
// объект для сериализации
Person person = new Person("Tom", 29);
Console.WriteLine("Объект создан");
// создаем объект BinaryFormatter
BinaryFormatter formatter = new BinaryFormatter();
// получаем поток, куда будем записывать сериализованный объект
using (FileStream fs = new FileStream("people.dat", FileMode.OpenOrCreate))
{
formatter.Serialize(fs, person);
Console.WriteLine("Объект сериализован");
}
// десериализация из файла people.dat
using (FileStream fs = new FileStream("people.dat", FileMode.OpenOrCreate))
{
Person newPerson = (Person)formatter.Deserialize(fs);
Console.WriteLine("Объект десериализован");
Console.WriteLine($"Имя: {newPerson.Name} --- Возраст: {newPerson.Age}");
}
Console.ReadLine();
}
}
}
Так как класс BinaryFormatter определен в пространстве имен System.Runtime.Serialization.Formatters.Binary
, то в самом начале подключаем его.
У нас есть простенький класс Person, который объявлен с атрибутом Serializable. Благодаря этому его объекты будут доступны для сериализации.
Далее создаем объект BinaryFormatter: BinaryFormatter formatter = new BinaryFormatter();
Затем последовательно выполняем сериализацию и десериализацию. Для обоих операций нам нужен поток, в который либо сохранять, либо из которого считывать данные. Данный поток представляет объект FileStream, который записывает нужный нам объект Person в файл people.dat
.
Сериализация одним методом formatter.Serialize(fs, person)
добавляет все данные об объекте Person в файл people.dat
.
При десериализации нам нужно еще преобразовать объект, возвращаемый функцией Deserialize, к типу Person: (Person)formatter.Deserialize(fs)
.
Как вы видите, сериализация значительно упрощает процесс сохранения объектов в бинарную форму по сравнению, например, с использованием связки классов BinaryWriter/BinaryReader.
Хотя мы взяли лишь один объект Person, но равным образом мы можем использовать и массив подобных объектов, список или иную коллекцию, к которой применяется атрибут Serializable. Посмотрим на примере массива:
Person person1 = new Person("Tom", 29);
Person person2 = new Person("Bill", 25);
// массив для сериализации
Person[] people = new Person[] { person1, person2 };
BinaryFormatter formatter = new BinaryFormatter();
using (FileStream fs = new FileStream("people.dat", FileMode.OpenOrCreate))
{
// сериализуем весь массив people
formatter.Serialize(fs, people);
Console.WriteLine("Объект сериализован");
}
// десериализация
using (FileStream fs = new FileStream("people.dat", FileMode.OpenOrCreate))
{
Person[] deserilizePeople = (Person[])formatter.Deserialize(fs);
foreach (Person p in deserilizePeople)
{
Console.WriteLine($"Имя: {p.Name} --- Возраст: {p.Age}");
}
}
Реализовать примеры из лекции. Привести текст примера и текст результата, например:
Конспект лекции "Работа с каталогами и файлами"
Список дисков
using System; using System.IO; DriveInfo[] drives = DriveInfo.GetDrives(); foreach (DriveInfo drive in drives) { Console.WriteLine($"Название: {drive.Name}"); Console.WriteLine($"Тип: {drive.DriveType}"); if (drive.IsReady) { Console.WriteLine($"Объем диска: {drive.TotalSize}"); Console.WriteLine($"Свободное пространство: {drive.TotalFreeSpace}"); Console.WriteLine($"Метка: {drive.VolumeLabel}"); } Console.WriteLine(); }
Результат работы:
/home/kei/RiderProjects/OAP/stream/bin/Debug/net8.0/stream Название: /proc Тип: Ram Объем диска: 0 Свободное пространство: 0 Метка: /proc Название: /sys Тип: Ram Объем диска: 0 Свободное пространство: 0 Метка: /sys Название: /dev Тип: Ram Объем диска: 8236191744 Свободное пространство: 8236191744 Метка: /dev Название: /run Тип: Ram Объем диска: 8254484480 Свободное пространство: 8252055552 Метка: /run Название: /sys/firmware/efi/efivars Тип: Unknown Объем диска: 188328 Свободное пространство: 107141 Метка: /sys/firmware/efi/efivars Название: /dev/pts Тип: Ram Объем диска: 0 Свободное пространство: 0 Метка: /dev/pts Название: / Тип: Fixed Объем диска: 1006662447104 Свободное пространство: 862912659456 Метка: / Название: /sys/kernel/security Тип: Ram Объем диска: 0 Свободное пространство: 0 Метка: /sys/kernel/security Название: /dev/shm Тип: Ram Объем диска: 8254484480 Свободное пространство: 8211234816 Метка: /dev/shm Название: /sys/fs/cgroup Тип: Ram Объем диска: 0 Свободное пространство: 0 Метка: /sys/fs/cgroup Название: /sys/fs/pstore Тип: Fixed Объем диска: 0 Свободное пространство: 0 Метка: /sys/fs/pstore Название: /sys/fs/bpf Тип: Fixed Объем диска: 0 Свободное пространство: 0 Метка: /sys/fs/bpf Название: /proc/sys/fs/binfmt_misc Тип: Ram Объем диска: 0 Свободное пространство: 0 Метка: /proc/sys/fs/binfmt_misc Название: /dev/mqueue Тип: Ram Объем диска: 0 Свободное пространство: 0 Метка: /dev/mqueue Название: /dev/hugepages Тип: Ram Объем диска: 0 Свободное пространство: 0 Метка: /dev/hugepages Название: /sys/kernel/debug Тип: Ram Объем диска: 0 Свободное пространство: 0 Метка: /sys/kernel/debug Название: /sys/kernel/tracing Тип: Fixed Объем диска: 0 Свободное пространство: 0 Метка: /sys/kernel/tracing Название: /sys/fs/fuse/connections Тип: Ram Объем диска: 0 Свободное пространство: 0 Метка: /sys/fs/fuse/connections Название: /sys/kernel/config Тип: Ram Объем диска: 0 Свободное пространство: 0 Метка: /sys/kernel/config Название: /tmp Тип: Ram Объем диска: 8254484480 Свободное пространство: 8238710784 Метка: /tmp Название: /boot/efi Тип: Fixed Объем диска: 313942016 Свободное пространство: 302096384 Метка: /boot/efi Название: /var/lib/docker/overlay2/4a2802316a49f1a30573524d0059ce49f6cd6cfe40608b77efb6febce3b2d66d/>merged Тип: Unknown Название: /var/lib/docker/overlay2/a91156ea1396c9882176076645afbc6b4c5ded5492d9226442a6c5b55d07fd67/>merged Тип: Unknown Название: /var/lib/docker/overlay2/de7b5cae3507be9b60ff7a3ddb8375e6968f465e3d926eb1c56c0206c8e2e17a/>merged Тип: Unknown Название: /var/lib/docker/overlay2/f6ab6ef9b79dabd97d12c898d4239d1fb173ebcd83a1abf5c22704e730390115/>merged Тип: Unknown Название: /var/lib/docker/overlay2/cf4ba0731412147587fe830cbcc73005b3f660443a73da88977222e55c304e27/>merged Тип: Unknown Название: /run/docker/netns/default Тип: Unknown Название: /run/docker/netns/fe8e9119c528 Тип: Unknown Название: /run/user/1000 Тип: Ram Объем диска: 1650896896 Свободное пространство: 1650798592 Метка: /run/user/1000 Название: /run/user/1000/doc Тип: Unknown Unhandled exception. System.UnauthorizedAccessException: Access to the path is denied. ---> System.IO.IOException: Operation not permitted --- End of inner exception stack trace --- at System.IO.DriveInfo.CheckStatfsResultAndThrowIfNecessary(Int32 result) at System.IO.DriveInfo.get_TotalSize() at Program.<Main>$(String[] args) in /home/kei/RiderProjects/OAP/stream/Program.cs:line 9 Process finished with exit code 134.
Предыдущая лекция | Следующая лекция | |
---|---|---|
Многопоточность. Потоки, асинхронные вычисления | Содержание | Типы файлов: CSV, XML, JSON. |