Содержание
Ввод-вывод строится на основе потоков. Большинство классов для работы с потоками и вводом-выводом находится в пространстве имен System.IO.
Потоковая архитектура
В основе потоковой архитектуры .NET лежат три понятия:
- опорное хранилище (backing store)
- декоратор (decorator)
- адаптер (adapter)
Опорное хранилище — это конечная точка ввода-вывода: файл, сетевое подключение и т.д. Оно может представлять собой либо источник, из которого последовательно считываются байты, либо приемник, куда байты последовательно записываются, либо и то и другое вместе.
Чтобы использовать опорное хранилище его нужно открыть. Этой цели и служат потоки, которые в .NET представлены классом System.IO.Stream, содержащий методы для чтения, записи и позиционирования потоков.
Потоки не загружают опорное хранилище в память целиком, а читают его последовательно по байтам либо блокам управляемого размера. Поэтому поток может потреблять мало памяти независимо от размера его опорного хранилища.
Потоки делятся на две категории:
- потоки опорных хранилищ — потоки, жестко привязанные к конкретным типам опорных хранилищ, такие как
FileStreamилиNetworkStream - потоки-декораторы — наполняют другие потоки, трансформируя данные тем или иным способом, такие как
DeflateStreamилиCryptoStream
Декораторы освобождают потоки опорных хранилищ от необходимости самостоятельно реализовывать такие вещи, как сжатие и шифрование. Декораторы можно подключать во время выполнения, а также соединять их в цепочки (т.е. использовать несколько декораторов в одном потоке).
Потоки работают с байтами. Это гибко и эффективно, но не всегда удобно, например, когда приложение имеет дело с текстом или XML. Преодолеть этот разрыв позволяютадаптеры, они помещают поток в оболочку класса со специальными методами для конкретного формата. В отличие от декораторов адаптеры сами не являются потоками, они обычно полностью скрывают байт-ориентированные методы.
Потоки
Абстрактный класс Stream является базовым для всех потоков. Он определяет методы и свойства для трех фундаментальных операций: чтение, запись и поиск, а также для выполнения служебных задач: закрытие, сброс, конфигурирование тайм-аутов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // Чтение: public abstract bool CanRead { get; } public abstract int Read (byte[] buffer, int offset, int count) public virtual int ReadByte (); // Запись: public abstract bool CanWrite { get; } public abstract void Write (byte[] buffer, int offset, int count); public virtual void WriteByte (byte value); // Поиск: public abstract bool CanSeek { get; } public abstract long Position { get; set; } public abstract void SetLength (long value); public abstract long Length { get; } public abstract long Seek (long offset, SeekOrigin origin); // Закрытие/сброс: public virtual void Close (); public void Dispose (); public abstract void Flush (); // Тайм-ауты: public virtual bool CanTimeout { get; } public virtual int ReadTimeout { get; set; } public virtual int WriteTimeout { get; set; } // Другие: public static readonly Stream Null; public static Stream Synchronized (Stream stream); |
Пример:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | using System; using System.IO; class Program { static void Main() { // CСоздать в текущем каталоге файл test.txt: using (Stream s = new FileStream ("test.txt", FileMode.Create)) { Console.WriteLine (s.CanRead); // True Console.WriteLine (s.CanWrite); // True Console.WriteLine (s.CanSeek); // True s.WriteByte (101); s.WriteByte (102); byte[] block = { 1, 2, 3, 4, 5 }; s.Write (block, 0, block.Length); // Записать блок из 5 байтов Console.WriteLine (s.Length); // 7 Console.WriteLine (s.Position); // 7 s.Position = 0; // Переместиться обратно в начало Console.WriteLine (s.ReadByte()); // 101 Console.WriteLine (s.ReadByte()); // 102 // Читать из потока в массив block: Console.WriteLine (s.Read (block, 0, block.Length)); // 5 Console.WriteLine (s.Read (block, 0, block.Length)); // 0 } } } |
Кроме того класс содержит асинхронные методы для чтения и записи: ReadAsync иWriteAsync:
1 2 3 4 5 6 7 8 9 10 11 | async static void AsyncDemo() { using (Stream s = new FileStream ("test.txt", FileMode.Create)) { byte[] block = { 1, 2, 3, 4, 5 }; await s.WriteAsync (block, 0, block.Length); // Асинхронная запись s.Position = 0; // Переместиться в начало // Читать из потока в массив block: Console.WriteLine (await s.ReadAsync (block, 0, block.Length)); // 5 } } |
Чтение и запись
Поток может поддерживать чтение, запись или то и другое. Если свойство CanWriteвозвращает false — поток предназначен только для чтения, если CanRead возвращаетfalse — поток предназначен только для записи.
Метод Read читает блок данных из потока и записывает их в массив. Он возвращает количество полученных байтов, которое может быть либо равно, либо меньше значения аргумента count. Если оно меньше count — это значит, что достигнут конец потока или поток выдает данные порциями меньшего размера (в связи с этим однозначно судить о том, что достигнут конец потока можно только если метод возвращает 0):
1 2 3 4 5 6 | byte[] data = new byte [1000]; // bytesRead в конце всегда будет равен 1000, если только сам поток не короче: int bytesRead = 0; int chunkSize = 1; while (bytesRead < data.Length && chunkSize > 0) bytesRead += chunkSize = s.Read (data, bytesRead, data.Length - bytesRead); |
Метод ReadByte читает один байт из потока, в случае достижения конца потока возвращает -1.
Методы Write и WriteByte отправляют данные в поток, в случае неудачи генерируют исключение.
Аргумент offset методов Read и Write ссылается на индекс в массиве buffer, с которого начинается чтение или запись, а не на позицию в потоке.
Позиционирование (Seeking)
Если свойство CanSeek возвращает true, то поток поддерживает возможность позиционирования. Для такого потока можно запрашивать свойство Length и модифицировать его с помощью метода SetLength. Также можно в любой момент изменять свойство Position, отражающее позицию относительно начала потока, в которой производится чтение или запись. Метод Seek позволяет перемещаться относительно текущей позиции или относительно конца потока.
Если поток не поддерживает возможность позиционирования, единственный способ определить длину потока — прочитать его до конца. К тому же, если требуется повторно прочитать предшествующую область, необходимо закрыть поток и начать все заново.
Закрытие и сброс
Потоки должны быть освобождены после использования, чтобы освободить лежащие в их основе ресурсы. Самый простой способ обеспечения этого — создание экземпляров потока внутри блока using.
Помимо этого потоки содержат методы Dispose и Close (функционально идентичны, закрывают поток). Многократное освобождение или закрытие потока не вызывает ошибки.
Закрытие потока с декоратором закрывает и декоратор и поток с опорным хранилищем. В случае цепочки декораторов закрытие самого внешнего декоратора закрывает всю цепочку.
Для улучшения производительности некоторые потоки буферизуют данные, поступающие в/из опорного хранилища. По этой причине данные записываемые в поток могут не сразу попадать в опорное хранилище, возможна задержка до заполнения буфера. Метод Flush обеспечивает принудительную запись любых буферизированных данных. При закрытие потока метод Flush вызывается автоматически.
Тайм-ауты
Если свойство CanTimeout возвращает true, поток поддерживает тайм-аут чтения и записи (тайм-ауты поддерживают сетевые потоки, а файловые и потоки в памяти — нет). Для таких потоков свойство ReadTimeout задает тайм-аут в миллисекундах на чтение, а свойство WriteTimeout — тайм-аут на запись. Ноль означает отсутствие тайм-аута. При наступлении тайм-аута методы Read и Write генерируют исключения.
Потоки с опорными хранилищами в .NET
Основными потоками с опорными хранилищами в .NET являются:
System.IO.FileStreamSystem.IO.MemoryStreamSystem.IO.IsolatedStorageFileStreamSystem.Net.Sockets.NetworkStreamSystem.IO.Pipes.PipeStream
Декораторы в .NET
Декораторы помещают другой поток в оболочку, добавляя ему различный функционал. Основными декораторами в .NET являются:
System.IO.BufferedStreamSystem.IO.Compression.DeflateStreamSystem.IO.Compression.GZipStreamSystem.Security.Cryptography.CryptoStreamSystem.Net.Security.AuthenticatedStream
Адаптеры потоков в .NET
Текстовые адаптеры (для типов string и char):
TextReaderTextWriterStreamReaderStreamWriterStringReaderStringWriter
Двоичные адаптеры (для типов int, bool, string и float):
BinaryReaderBinaryWriter
Адаптеры XML:
XmlReaderXmlWriter
FileStream
Создать экземпляр FileStream можно с помощью статических методов класса File:
1 2 3 | FileStream fs1 = File.OpenRead ("readme.bin"); // Только для чтения FileStream fs2 = File.OpenWrite (@"c:\temp\writeme.tmp"); // Только для записи FileStream fs3 = File.Create (@"c:\temp\writeme.tmp"); // Чтение и запись |
Методы OpenWrite и Create ведут себя по разному если файл уже существует: метод Create удалит все содержимое существующего файла, а метод OpenWriteоставит содержимое файла не тронутым, установив позицию потока в ноль (если будет записано меньше байтов, чем существовало в файле, метод оставит смесь старого и нового содержимого).
Можно создать экземпляр FileStream с помощью конструктора класса FileStream, которому можно передать имя файла или файловый дескриптор, режимы создания и доступа к файлу, опции для совместного использования, буферизации и безопасности:
1 | var fs = new FileStream ("readwrite.tmp", FileMode.Open); // Чтение и запись |
Класс File определяет также более удобные статические методы, позволяющие открыть файл и прочитать/записать его содержимое за одни шаг:
File.ReadAllText— читает целый файл и возвращает его содержимое в виде одной строкиFile.ReadAllLines— читает целый файл и возвращает его содержимое в виде массива строкFile.ReadAllBytes— читает целый файл и возвращает его содержимое в виде байтового массиваFile.WriteAllText— записывает строку в файлFile.WriteAllLines— записывает массив строк в файлFile.WriteAllBytes— записывает байтовый массив в файлFile.AppendAllText— добавляет строку в конец файла
Метод File.ReadLines не загружает весь файл в память, а возвращает лениво-оцениваемое IEnumerable<string>:
1 | int longLines = File.ReadLines ("filePath").Count (l => l.Length > 80); |
Имя файла
Имя файла может быть абсолютным или относительным к текущему каталогу. Узнать текущий каталог или изменить его можно с помощью статического свойстваEnvironment.CurrentDirectory. Текущий каталог может как совпадать, так и не совпадать с директорией, в которой расположен исполняемый файл программы.
Свойство AppDomain.CurrentDomain.BaseDirectory возвращает базовый каталог приложения, который как правило совпадает с директорией, содержащей исполняемый файл программы.
Чтобы указать имя файла относительно полученного каталога, можно использовать метод Path.Combine:
1 2 3 | string baseFolder = AppDomain.CurrentDomain.BaseDirectory; string logoPath = Path.Combine (baseFolder, "logo.jpg"); Console.WriteLine (File.Exists (logoPath)); |
Допустимо использовать сетевые имена, такие как \\JoesPC\PicShare\pic.jpgили \\10.1.1.2\PicShare\pic.jpg.
Режимы файла (FileMode)
Все конструкторы FileStream помимо имени файла требуют указания режима файла — значения enum FileMode:
FileMode.CreateNew— будет создан новый файл, доступный для чтения и записи; если файл с таким именем уже существует, будет выброшено исключениеFileMode.Create— тоже самое, что иFileMode.CreateNew, но если файл с таким именем уже существует, он будет перезаписан новым (содержимое существующего файла будет обнулено)FileMode.OpenOrCreate— если файл существует — он будет открыт для чтения и записи, если не существует — будет создан новый (доступный для чтения и записи)FileMode.Open— файл открывается для чтения и записи; если файла с таким именем не существует, будет выброшено исключениеFileMode.Truncate— тоже самое, что иFileMode.Open, но содержимое открытого файла обнуляетсяFileMode.Append— файл открывается только для записи; запись производится в конец файла
Метод File.Create и значение FileMode.Create приведут к генерации исключения, если используются для скрытых файлов. Чтобы перезаписать скрытый файл, его придется сначала удалить и создать заново.
Во всех случаях, кроме FileMode.Append, поток открывается для чтения и записи. Можно ограничить уровень доступа, если в качестве дополнительного параметра передать enum FileAccess:
1 2 | [Flags] public enum FileAccess { Read = 1, Write = 2, ReadWrite = 3 } |
При создании экземпляра FileStream можно также указывать следующие дополнительные аргументы:
enum FileShare— уровень доступа для других процессов, пока мы работает с файлом (None,Read(по умолчанию),ReadWriteилиWrite)- размер внутреннего буфера в байтах
- флаг, указывающий следует ли отложить асинхронный вывод для ОС
- объект
FileSecurity, описывающий права доступа для пользователей для создаваемого файла enum FileOptions:FileOptions.Encrypted— включает шифрование на уровне ОСFileOptions.DeleteOnClose— автоматическое удаление при закрытии временных файловFileOptions.RandomAccessиFileOptions.SequentialScanFileOptions.WriteThrough— отключение кэширования при записи
Если файл открыт с ключом FileShare.ReadWrite позволяет другим процессам читать и записывать файл. С помощью методов класса FileStream Lock и Unlockможно блокировать и разблокировать участки файла:
1 2 | public virtual void Lock (long position, long length); public virtual void Unlock (long position, long length); |
Если часть или вся запрошенная область файла уже заблокированы метод Lockвыбросит исключение.
MemoryStream
Класс MemoryStream в качестве опорного хранилища использует массив (целиком расположенный в памяти).
С помощью методов ToArray и GetBuffer можно преобразовать MemoryStream в байтовый массив.
Закрытие и сброс MemoryStream являются не обязательными. После закрытия выполнять чтение и запись потока нельзя. Метод Flush потока MemoryStreamвообще ничего не делает.
PipeStream
Класс PipeStream предоставляет простой способ взаимодействия одного процесса с другим через протокол каналов Windows. Канал — это низкоуровневая конструкция, которая позволяет отправлять и получать байты (или сообщения — группы байтов). Различают два вида каналов:
- анонимный канал — обеспечивает однонаправленное взаимодействие между родительским и дочерним процессом на одном компьютере
- именованный канал — обеспечивает двунаправленное взаимодействие между произвольными процессами на одном компьютере или на разных (через сеть)
PipeStream является абстрактным классом с четырьмя конкретными подтипами: два применяются для анонимных каналов (AnonymousPipeServerStream иAnonymousPipeClientStream), два для именованных (NamedPipeServerStream и NamedPipeClientStream).
Именованные потоки
В случае именованных потоков участники взаимодействуют через канал с одним и тем же именем. Протокол определяет две отдельные роли: клиент и сервер, взаимодействие между которыми происходит следующим образом:
- сервер создает экземпляр
NamedPipeServerStreamи вызывает его методWaitForConnection - клиент создает экземпляр
NamedPipeClientStreamи вызывает его методConnect(с необязательным тайм-аутом)
После этого участники для взаимодействия осуществляют чтение и запись в потоки:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // Сервер: using (var s = new NamedPipeServerStream ("pipedream")) { s.WaitForConnection(); s.WriteByte (100); Console.WriteLine (s.ReadByte()); } // Клиент: using (var s = new NamedPipeClientStream ("pipedream")) { s.Connect(); Console.WriteLine (s.ReadByte()); s.WriteByte (200); } |
Для поддержки сообщений длиннее одного байта каналы предлагают режим передачи сообщений. Когда он включен, участник читающий поток, может узнать, что сообщение завершено, проверив свойство потока IsMessageComplete. Включить режим передачи сообщений на стороне сервера можно указав PipeTransmissionMode.Message при создании экземпляра потока, а на стороне клиента — присвоив значение PipeTransmissionMode.Message свойству потока ReadMode после вызова Connect:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | // Сервер: using (var s = new NamedPipeServerStream ("pipedream", PipeDirection.InOut, 1, PipeTransmissionMode.Message)) { s.WaitForConnection(); byte[] msg = Encoding.UTF8.GetBytes ("Hello"); s.Write (msg, 0, msg.Length); Console.WriteLine (Encoding.UTF8.GetString (ReadMessage (s))); } // Клиент: using (var s = new NamedPipeClientStream ("pipedream")) { s.Connect(); s.ReadMode = PipeTransmissionMode.Message; Console.WriteLine (Encoding.UTF8.GetString (ReadMessage (s))); byte[] msg = Encoding.UTF8.GetBytes ("Hello right back!"); s.Write (msg, 0, msg.Length); } // Метод для чтения потока на клиенте: static byte[] ReadMessage (PipeStream s) { MemoryStream ms = new MemoryStream(); byte[] buffer = new byte [0x1000]; do { ms.Write (buffer, 0, s.Read (buffer, 0, buffer.Length)); } while (!s.IsMessageComplete); return ms.ToArray(); } |
Определить, завершил ли поток PipeStream чтение сообщения, нельзя за счет простого ожидания, когда Read вернет 0, т.к. потоки каналов и сетевые потоки не имеют определенного окончания, а между передачами сообщения они опустошаются.
Клиент и сервер должны следовать определенному протоколу для координации своих действий, чтобы оба участника не начали одновременно принимать или отправлять данные.
Анонимные потоки
Анонимные каналы вместо имени используют закрытый дескриптор. Они также предполагают две роли: клиент и сервер, но взаимодействие осуществляется иначе:
- сервер создает экземпляр
AnonymousPipeServerStreamи устанавливает его направление через свойствоPipeDirection, которое может бытьInилиOut - сервер вызывает метод
GetClientHandleAsStringдля получения дескриптора канала, который затем передается клиенту (обычно как аргумент при запуске дочернего процесса) - клиент создает экземпляр
AnonymousPipeClientStream, указывая ему противоположное направление (свойствоPipeDirection) - родительский и дочерний процессы взаимодействуют посредством чтения и записи в потоки
- сервер освобождает локальный дескриптор за счет вызова метода
DisposeLocalCopyOfClientHandle
Поскольку анонимные каналы являются однонаправленными, для реализации двунаправленного взаимодействия сервер должен создать два канала.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // Сервер: string clientExe = @"d:\PipeDemo\ClientDemo.exe"; HandleInheritability inherit = HandleInheritability.Inheritable; using (var tx = new AnonymousPipeServerStream (PipeDirection.Out, inherit)) using (var rx = new AnonymousPipeServerStream (PipeDirection.In, inherit)) { string txID = tx.GetClientHandleAsString(); string rxID = rx.GetClientHandleAsString(); var startInfo = new ProcessStartInfo (clientExe, txID + " " + rxID); startInfo.UseShellExecute = false; // Требуется для дочернего процесса Process p = Process.Start (startInfo); tx.DisposeLocalCopyOfClientHandle(); rx.DisposeLocalCopyOfClientHandle(); tx.WriteByte (100); Console.WriteLine ("Server received: " + rx.ReadByte()); p.WaitForExit(); } // Клиент: string rxID = args[0]; string txID = args[1]; using (var rx = new AnonymousPipeClientStream (PipeDirection.In, rxID)) using (var tx = new AnonymousPipeClientStream (PipeDirection.Out, txID)) { Console.WriteLine ("Client received: " + rx.ReadByte()); tx.WriteByte (200); } |
Клиент и сервер в случае анонимных каналов также должны координировать свои действия по отправке и получению данных, а также согласовать длину каждой передачи (анонимные каналы не поддерживают режим передачи сообщений).
BufferedStream
Класс BufferedStream является декоратором, он помещает другой поток в оболочку и добавляет ему возможность буферизации. Буферизация улучшает производительность, сокращая количество двусторонних обменов с опорным хранилищем.
1 2 3 4 5 6 7 8 | // Записать 100K в файл: File.WriteAllBytes ("myFile.bin", new byte [100000]); using (FileStream fs = File.OpenRead ("myFile.bin")) using (BufferedStream bs = new BufferedStream (fs, 20000)) // буфер размером 20K { bs.ReadByte(); Console.WriteLine (fs.Position); // 20000 } |
В примере поток FileStream помещается в декоратор BufferedStream с буфером в 20Kb. При вызова ReadByte фактически из потока FileStream считывается не один байт, а 20 000 байтов (размер буфера), которые помещаются в буфер. При следующих 19 999 вызовах метода ReadByte данные будут читаться из буфера, а не из файлового потока. На практике соединение FileStream с BufferedStreamизбыточно, так как FileStream сам поддерживает буферизацию.
Закрытие BufferedStream автоматически закрывает лежащий в основе поток с опорным хранилищем.
Текстовые адаптеры
TextReader и TextWriter
TextReader и TextWriter являются абстрактными базовыми классами для адаптеров, которые имеют дело исключительно с символами и строками. Каждый из них имеет две реализации:
StreamReaderиStreamWriter— для хранения данных используют классStreamи транслируют байты потока в символы или строкиStringReaderиStringWriter— для хранения данных используют строки
Члены класса TextReader:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // Чтение одного символа: public virtual int Peek (); // Результат приводится к char public virtual int Read (); // Результат приводится к char // Чтение нескольких символов: public virtual int Read (char[] buffer, int index, int count); public virtual int ReadBlock (char[] buffer, int index, int count); public virtual string ReadLine (); public virtual string ReadToEnd (); // Закрытие: public virtual void Close (); public void Dispose (); // Другие: public static readonly TextReader Null; public static TextReader Synchronized (TextReader reader); |
Метод Peek возвращает следующий символ из потока, не перемещая текущую позицию вперед. Метод Peek и Read (без аргументов) возвращают -1 при достижении конца потока или целочисленное значение, которое может быть приведено к char.
Перегруженная версия Read, принимающая буфер char[], идентична по функционалу методу ReadBlock.
Метод ReadLine выполняет чтение до тех пор, пока не встретит символы новой строки (CR, LF либо CR+LF). Он возвращает строку отбрасывая символы новой строки.
Члены класса TextWriter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // Запись одного символа: public virtual void Write (char value); // Запись нескольких символов: public virtual void Write (string value); public virtual void Write (char[] buffer, int index, int count); public virtual void Write (string format, params object[] arg); public virtual void WriteLine (string value); // Закрытие и сброс: public virtual void Close (); public void Dispose (); public virtual void Flush (); // Форматирование и кодировка: public virtual IFormatProvider FormatProvider { get; } public virtual string NewLine { get; set; } public abstract Encoding Encoding { get; } // Другие: public static readonly TextWriter Null; public static TextWriter Synchronized (TextWriter writer); |
Методы Write и WriteLine дополнительно перегружены, чтобы принимать каждый из примитивных типов, а также тип object (они просто вызывают ToString на том, что им передано).
Метод WriteLine дополняет переданную ему строку последовательностью CR+LF. Свойство NewLine позволяет заменить эту последовательность на другие символы.
Классы TextReader и TextWriter также определяют асинхронные версии методов чтения и записи.
StreamReader и StreamWriter
Сами по себе классы TextReader и TextWriter являются абстрактными и никак не связаны ни с потоком, ни с опорным хранилищем. А вот реализующие их типы StreamReader и StreamWriter имеют в своей основе байтовый поток и выполняют преобразование между символами и байтами.
Для создания экземпляра StreamReader или StreamWriter их конструкторам необходимо передать байтовый поток:
1 2 3 4 5 6 7 8 9 10 11 12 | using (FileStream fs = File.Create ("test.txt")) using (TextWriter writer = new StreamWriter (fs)) { writer.WriteLine ("Line1"); writer.WriteLine ("Line2"); } using (FileStream fs = File.OpenRead ("test.txt")) using (TextReader reader = new StreamReader (fs)) { Console.WriteLine (reader.ReadLine()); Console.WriteLine (reader.ReadLine()); } |
Класс File предоставляет статические методы CreateText, AppendText и OpenText, которые позволяют создать экземпляр StreamReader или StreamWriter более лаконично:
1 2 3 4 5 6 7 8 9 10 | using (TextWriter writer = File.CreateText ("test.txt")) { writer.WriteLine ("Line1"); writer.WriteLine ("Line2"); } using (TextWriter writer = File.AppendText ("test.txt")) writer.WriteLine ("Line3"); using (TextReader reader = File.OpenText ("test.txt")) while (reader.Peek() > −1) Console.WriteLine (reader.ReadLine()); |
Достижение конца файла можно проверить с помощью метода Peek (вернет -1) или метода ReadLine (вернет null).
Преобразование байтов потока в строку выполняется с помощью класса System.Text.Encoding, который можно передать в конструктор StreamReader или StreamWriter. По умолчанию используется кодировка UTF-8.
1 2 3 | using (Stream s = File.Create ("but.txt")) using (TextWriter w = new StreamWriter (s, Encoding.Unicode)) w.WriteLine ("but-"); |
StringReader и StringWriter
Адаптеры StringReader и StringWriter вообще не имеют дела с потоками. Вместо этого они используют в качестве лежащего в основе источника данных строку или экземпляр StringBuilder. Они не выполняют никаких преобразований байтов в строку и обратно, по сути они не делают ничего такого, что нельзя было бы сделать с помощью строк и StringBuilder. Преимущество же их в том, что они имеют общий базовый класс с типами StreamReader и StreamWriter, облегчая их преобразования.
Двоичные адаптеры
Классы BinaryReader и BinaryWriter выполняют чтение и запись в поток предопределенных типов: bool, byte, char, decimal, float, double, short, int,long, sbyte, ushort, uint и ulong, а также строк и массивов предопределенных типов. В отличие от текстовых адаптеров двоичные адаптеры сохраняют предопределенные символы эффективнее, так как они представлены в памяти.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public class Person { public string Name; public int Age; public double Height; public void SaveData (Stream s) { var w = new BinaryWriter (s); w.Write (Name); w.Write (Age); w.Write (Height); w.Flush(); } public void LoadData (Stream s) { var r = new BinaryReader (s); Name = r.ReadString(); Age = r.ReadInt32(); Height = r.ReadDouble(); } } |
Класс BinaryReader может также выполнять чтение в байтовый массив:
1 | byte[] data = new BinaryReader(s).ReadBytes ((int) s.Length); |
Закрытие адаптеров потока
Закрытие адаптера приводит к автоматическому закрытию лежащего в основе потока:
1 2 3 | using (FileStream fs = File.Create ("test.txt")) using (TextWriter writer = new StreamWriter (fs)) writer.WriteLine ("Line"); |
При этом если в конструкторе адаптера будет выброшено исключение, поток все равно закроется.
При закрытии адаптера и потока без использования инструкции using, нужно всегда сначала закрывать или сбрасывать адаптер, и только после этого закрывать поток, иначе любые данные, буферизированные в адаптере, будут утеряны.
Адаптеры относятся к необязательно освобождаемым объектам. Это означает, что их необязательно закрывать перед закрытием потока. В большинстве случаев их достаточно просто сбросить. Это может быть удобно в ситуации, когда после завершения работы с адаптером лежащий в его основе поток должен остаться для дальнейшей работы с ним:
1 2 3 4 5 6 7 8 | using (FileStream fs = new FileStream ("test.txt", FileMode.Create)) { StreamWriter writer = new StreamWriter (fs); writer.WriteLine ("Hello"); writer.Flush(); fs.Position = 0; Console.WriteLine (fs.ReadByte()); } |
В примере если вместо сброса адаптера закрыть его, лежащий в его основе поток тоже будет закрыт, что приведет к сбою последующих операций над потоком.
Если конструкторы адаптера StreamReader/StreamWriter в качестве четвертого параметра передать true, то после его освобождения лежащий в основе поток закрыт не будет:
1 2 3 4 5 6 7 8 9 10 11 | using (var fs = new FileStream ("test.txt", FileMode.Create)) { using (var writer = new StreamWriter (fs, new UTF8Encoding (false, true), 0x400, true)) writer.WriteLine ("Hello"); fs.Position = 0; Console.WriteLine (fs.ReadByte()); Console.WriteLine (fs.Length); } |
Сжатие потоков
В пространстве имен System.IO.Compression доступно два декоратора для сжатия потоков: DeflateStream и GZipStream. Отличаются они тем, что GZipStreamзаписывает дополнительную информацию в начале и в конце, а также соответствует стандарту. Оба класса являются декораторами: они сжимают или распаковывают данные из другого потока, который указывается при создании их экземпляра:
1 2 3 4 5 6 7 8 | using (Stream s = File.Create ("compressed.bin")) using (Stream ds = new DeflateStream (s, CompressionMode.Compress)) for (byte i = 0; i < 100; i++) ds.WriteByte (i); using (Stream s = File.OpenRead ("compressed.bin")) using (Stream ds = new DeflateStream (s, CompressionMode.Decompress)) for (byte i = 0; i < 100; i++) Console.WriteLine (ds.ReadByte()); |
В этом же пространстве имен определяются еще два класса: ZipArchive и ZipFile, которые использую популярный алгоритм сжатия, применяемый в zip-файлах, что делает их совместимыми с zip-файлами, созданными в других приложениях, а также позволяет сжимать несколько файлов в один архив.
Класс ZipArchive работает непосредственно с потоками, а ZipFile является статическим вспомогательным классом для него. ZipFile более удобен в использовании при работе с файлами.
Метод CreateFromDirectory класса ZipFile добавляет все файлы из указанного каталога в zip-архив:
1 | ZipFile.CreateFromDirectory (@"d:\MyFolder", @"d:\compressed.zip"); |
Метод ExtractToDirectory того же класса извлекает содержимое zip-архива в указанный каталог:
1 | ZipFile.ExtractToDirectory (@"d:\compressed.zip", @"d:\MyFolder"); |
При сжатии можно задать оптимизацию по размеру файла или по скорости сжатия, а также необходимость включения в архив исходных директории.
Экземпляр ZipArchive можно создать либо с помощью конструктора, передав ему поток (объект Stream), либо с помощью статического метода Open класс ZipFile, передав ему имя файла и действие, которое должно быть произведено над архивом —Read (чтение), Create (создание) или Update (обновление). Свойство Entriesобъекта ZipArchive возвращает коллекцию входящих в архив файлов, а метод GetEntry позволяет найти конкретный файл:
1 2 3 | using (ZipArchive zip = ZipFile.Open (@"d:\zz.zip", ZipArchiveMode.Read)) foreach (ZipArchiveEntry entry in zip.Entries) Console.WriteLine (entry.FullName + " " + entry.Length); |
Класс ZipArchiveEntry инкапсулирует отдельный файл в архиве. Он имеет методыDelete (позволяет удалить файл из архива), ExtractToFile (позволяет извлечь файл из архива) и Open (возвращает экземпляр Stream, с возможностью чтения/записи). Создавать новые файлы в архиве можно с помощью методов CreateEntry иCreateEntryFromFile класса ZipArchive.
1 2 3 | byte[] data = File.ReadAllBytes (@"d:\foo.dll"); using (ZipArchive zip = ZipFile.Open (@"d:\zz.zip", ZipArchiveMode.Update)) zip.CreateEntry (@"bin\X64\foo.dll").Open().Write (data, 0, data.Length); |
Манипулирование файлами и каталогами
Пространство имен System.IO содержит ряд типов, предназначенных для манипулирования файлами и каталогами, позволяющих копировать, перемещать, создавать файлы и каталоги, устанавливать их атрибуты и права доступа. Сюда входят статические классы File и Directory, экземплярные классы FileInfo иDirectoryInfo, а также статический класс Path, позволяющий манипулировать строками путей к файлам и каталогам.
Класс File
File — статический класс, все методы которого принимают имя файла. Имя файла может быть абсолютным или относительным (относительно текущего каталога). Класс включает следующие статические методы:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | bool Exists (string path); // Возвращает true если файл существует void Delete (string path); void Copy (string sourceFileName, string destFileName); void Move (string sourceFileName, string destFileName); void Replace (string sourceFileName, string destinationFileName, string destinationBackupFileName); FileAttributes GetAttributes (string path); void SetAttributes (string path, FileAttributes fileAttributes); void Decrypt (string path); void Encrypt (string path); DateTime GetCreationTime (string path); DateTime GetLastAccessTime (string path); DateTime GetLastWriteTime (string path); void SetCreationTime (string path, DateTime creationTime); void SetLastAccessTime (string path, DateTime lastAccessTime); void SetLastWriteTime (string path, DateTime lastWriteTime); FileSecurity GetAccessControl (string path); FileSecurity GetAccessControl (string path, AccessControlSections includeSections); void SetAccessControl (string path, FileSecurity fileSecurity); |
Методы Move и Replace позволяют переименовать файл или переместить его в другой каталог, при этом Move генерирует исключение, если файл назначения существует, а Replace этого не делает.
Метод Delete удаляет файл, а если файл помечен как предназначенный только для чтения метод сгенерирует исключение UnauthorizedAccessException.
Метод GetAttributes возвращает enum FileAttribute со следующими значениями, которые можно комбинировать:
ArchiveCompressedDeviceDirectoryEncryptedHiddenNormalNotContentIndexedOfflineReadOnlyReparsePointSparseFileSystemTemporary
С помощью метода SetAttributes атрибуты файла можно менять:
1 2 3 4 5 6 7 8 9 | string filePath = @"c:\temp\test.txt"; FileAttributes fa = File.GetAttributes (filePath); if ((fa & FileAttributes.ReadOnly) > 0) { fa ^= FileAttributes.ReadOnly; File.SetAttributes (filePath, fa); } // теперь файл можно, например, удалить File.Delete (filePath); |
С помощью класса FileInfo это можно сделать лаконичней:
1 | new FileInfo (@"c:\temp\test.txt").IsReadOnly = false; |
Атрибуты Compressed и Encrypted (сжатие и шифрование) с помощью методаSetAttribute изменить нельзя. Для шифрования и дешифрования предназначены методы Encrypt и Decrypt класса File. Для сжатия класс File методов не содержит.
Методы GetAccessControl и SetAccessControl позволяют получать и задавать права доступа ОС через объект FileSecurity (пространство имен System.Security.AccessControl). Этот объект также можно передать в конструктор FileStream для указания прав доступа при создании файла.
Класс Directory
Статический класс Directory содержит методы аналогичные методам классаFile: для проверки существования каталога (Exists), перемещения каталога (Move), удаления каталога (Delete), получения/установки времени создания и последнего доступа, получения/установки прав доступа. Кроме того Directory включает следующие статические методы:
1 2 3 4 5 6 7 8 9 10 11 12 13 | string GetCurrentDirectory (); void SetCurrentDirectory (string path); DirectoryInfo CreateDirectory (string path); DirectoryInfo GetParent (string path); string GetDirectoryRoot (string path); string[] GetLogicalDrives (); // Все следующие методы возвращают полный путь: string[] GetFiles (string path); string[] GetDirectories (string path); string[] GetFileSystemEntries (string path); IEnumerable<string> EnumerateFiles (string path); IEnumerable<string> EnumerateDirectories (string path); IEnumerable<string> EnumerateFileSystemEntries (string path); |
Методы Enumerate* эффективней методов Get*, поскольку извлекают данные только при перечислении. И те и другие перегружены и могут принимать searchPattern(string) и searchOption (enum). При указании SearchOption.SearchAllSubDirectories также будет выполняться рекурсивный поиск в подкаталогах. Методы *FileSystemEntries сочетают в себе функционал методов *Files и *Directories.
FileInfo и DirectoryInfo
Если над файлом или каталогом требуется выполнить последовательность операций, то более удобными будут экземплярные методы FileInfo и DirectoryInfo.
Класс FileInfo предлагает большинство статических методов класс File в форме экземплярных методов, а также содержит ряд дополнительных свойств — Extension,Length, IsReadOnly и Directory. Последний возвращает объект DirectoryInfo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | FileInfo fi = new FileInfo (@"c:\temp\FileInfo.txt"); Console.WriteLine (fi.Exists); // false using (TextWriter w = fi.CreateText()) w.Write ("Some text"); Console.WriteLine (fi.Exists); // по прежнему false fi.Refresh(); Console.WriteLine (fi.Exists); // true Console.WriteLine (fi.Name); // FileInfo.txt Console.WriteLine (fi.FullName); // c:\temp\FileInfo.txt Console.WriteLine (fi.DirectoryName); // c:\temp Console.WriteLine (fi.Directory.Name); // temp Console.WriteLine (fi.Extension); // .txt Console.WriteLine (fi.Length); // 9 fi.Encrypt(); fi.Attributes ^= FileAttributes.Hidden; // Переключает флаг "скрытый" fi.IsReadOnly = true; Console.WriteLine (fi.Attributes); // ReadOnly, Archive, Hidden, Encrypted Console.WriteLine (fi.CreationTime); fi.MoveTo (@"c:\temp\FileInfoX.txt"); DirectoryInfo di = fi.Directory; Console.WriteLine (di.Name); // temp Console.WriteLine (di.FullName); // c:\temp Console.WriteLine (di.Parent.FullName); // c:\ di.CreateSubdirectory ("SubFolder"); |
Пример использования класса DirectoryInfo:
1 2 3 4 5 | DirectoryInfo di = new DirectoryInfo (@"e:\photos"); foreach (FileInfo fi in di.GetFiles ("*.jpg")) Console.WriteLine (fi.Name); foreach (DirectoryInfo subDir in di.GetDirectories()) Console.WriteLine (subDir.FullName); |
Path
Статический класс Path содержит методы и поля для работы с путями и именами файлов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | string dir = @"c:\mydir"; string file = "myfile.txt"; string path = @"c:\mydir\myfile.txt"; Directory.SetCurrentDirectory (@"k:\demo"); // Методы Path: Path.IsPathRooted (file) // False Path.IsPathRooted (path) // True Path.GetPathRoot (path) // c:\ Path.GetDirectoryName (path) // c:\mydir Path.GetFileName (path) // myfile.txt Path.GetFullPath (file) // k:\demo\myfile.txt Path.Combine (dir, file) // c:\mydir\myfile.txt // Расширения файлов: Path.HasExtension (file) // True Path.GetExtension (file) // .txt Path.GetFileNameWithoutExtension (file) // myfile Path.ChangeExtension (file, ".log") // myfile.log // Разделители и символы: Path.AltDirectorySeparatorChar // / Path.PathSeparator // ; Path.VolumeSeparatorChar // : Path.GetInvalidPathChars() // символы от 0 до 31 и "<>| Path.GetInvalidFileNameChars() // символы от 0 до 31 и "<>|:*?\/ // Временные файлы: Path.GetTempPath() // <local user folder>\Temp Path.GetRandomFileName() // d2dwuzjf.dnp Path.GetTempFileName() // <local user folder>\Temp\tmp14B.tmp |
Метод Combine объединяет каталог и имя файла, при необходимости дополняя имя каталога косой чертой. Метод GetFullPath преобразует относительный путь в абсолютный. Метод GetRandomFileName возвращает уникальное случайное имя файла в формате 8.3, не создавая при этом файл. Метод GetTempFileName генерирует временное имя файла с применением автоинкрементного счетчика и затем создает пустой файл с этим именем в локальном временном каталоге.
Специальные директории
В перечисленных выше классах отсутствуют средства нахождения специальных директорий Windows, таких как My Documents, Program Files и др. Сделать это можно с помощью метода GetFolderPath класса System.Environment:
1 2 | string myDocPath = Environment.GetFolderPath (Environment.SpecialFolder.MyDocuments); |
Значения enum Environment.SpecialFolder охватывают все специальные каталоги в Windows:
AdminToolsCommonVideosPersonalApplicationDataCookiesPrinterShortcutsCDBurningDesktopProgramFilesCommonAdminToolsDesktopDirectoryProgramFilesX86CommonApplicationDataFavoritesProgramsCommonDesktopDirectoryFontsRecentCommonDocumentsHistoryResourcesCommonMusicInternetCacheSendToCommonOemLinksLocalApplicationDataStartMenuCommonPicturesLocalizedResourcesStartupCommonProgramFilesMyComputerSystemCommonProgramFilesX86MyDocumentsSystemX86CommonProgramsMyMusicTemplatesCommonStartMenuMyPicturesUserProfileCommonStartupMyVideosWindowsCommonTemplatesNetworkShortcuts
Использование папок ApplicationData (настройки, перемещаемые с пользователем по сети), LocalApplicationData (неперемещаемые настройки пользователя), CommonApplicationData (настройки для всех пользователей компьютера) для сохранения данных и настроек приложения более предпочтительно, чем использование системного реестра. Как правило в этих директориях создаются подкаталоги с именем, совпадающим с названием приложения.
Информация о диске
Запрашивать информацию об устройствах на компьютере можно с помощью класса DriveInfo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | DriveInfo c = new DriveInfo ("C"); // Запросить диск C: long totalSize = c.TotalSize; // Размер в байтах long freeBytes = c.TotalFreeSpace; // Игнорирует дисковую квоту long freeToMe = c.AvailableFreeSpace; // Учитывает дисковую квоту foreach (DriveInfo d in DriveInfo.GetDrives()) // Все устройства хранения данных { Console.WriteLine (d.Name); // C:\ Console.WriteLine (d.DriveType); // Жесткий диск Console.WriteLine (d.RootDirectory); // C:\ if (d.IsReady) // Если устройство не готово следующие свойства выбросят исключение { Console.WriteLine (d.VolumeLabel); // System Console.WriteLine (d.DriveFormat); // NTFS } } |
enum DriveType содержит следующие значения:
UnknownNoRootDirectoryRemovableFixedNetworkCDRomRam
Перехват событий файловой системы
Класс FileSystemWatcher позволяет отслеживать действия над каталогами и их подкаталогами: создание, модификация, переименование, удаление файлов и поддиректорий, а также изменение их атрибутов. Событие генерируется независимо от того, кем оно совершено — пользователем или процессом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | static void Main() { Watch (@"c:\temp", "*.txt", true); } static void Watch (string path, string filter, bool includeSubDirs) { using (var watcher = new FileSystemWatcher (path, filter)) { watcher.Created += FileCreatedChangedDeleted; watcher.Changed += FileCreatedChangedDeleted; watcher.Deleted += FileCreatedChangedDeleted; watcher.Renamed += FileRenamed; watcher.Error += FileError; watcher.IncludeSubdirectories = includeSubDirs; watcher.EnableRaisingEvents = true; Console.WriteLine ("Listening for events - press <enter> to end"); Console.ReadLine(); } } static void FileCreatedChangedDeleted (object o, FileSystemEventArgs e) { Console.WriteLine ("File {0} has been {1}", e.FullPath, e.ChangeType); } static void FileRenamed (object o, RenamedEventArgs e) { Console.WriteLine ("Renamed: {0}->{1}", e.OldFullPath, e.FullPath); } static void FileError (object o, ErrorEventArgs e) { Console.WriteLine ("Error: " + e.GetException().Message); } |
Размещенные в памяти файлы (Memory-Mapped Files)
Типы для размещенных в памяти файлов находятся в пространстве имен System.IO.MemoryMappedFiles. Размещенные в памяти файлы дают две ключевые возможности:
- эффективный произвольный доступ к файловым данным
- возможность разделения памяти между различными процессами на одном компьютере
Хотя класс FileStream дает возможность произвольного ввода-вывода, он оптимизирован для последовательного ввода-вывода. Как правило он в 10 раз быстрее при последовательном вводе-выводе и в 10 раз медленнее при произвольном чем файлы размещенные в памяти.
Чтобы создать размещенный в памяти файл необходимо:
- получить объект
FileStreams - создать экземпляр
MemoryMappedFile, передав конструктору файловый поток - вызвать метод
CreateViewAccessorна объектеMemoryMappedFile
В результате последнего действия будет получен объект MemoryMappedViewAccessor, предоставляющий методы для произвольного чтения и записи простых типов, структур и массивов.
1 2 3 4 5 6 7 | File.WriteAllBytes ("long.bin", new byte [1000000]); using (MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile ("long.bin")) using (MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor()) { accessor.Write (500000, (byte) 77); Console.WriteLine (accessor.ReadByte (500000)); // 77 } |
Методу CreateFromFile можно также передать имя размещенного в памяти файла и емкость.
1 2 | using (var mmf = MemoryMappedFile.CreateFromFile ("long.bin", FileMode.Create, null, 1000)) |
Указание имени отличного от null позволяет разделять блок памяти с другими процессами. Хотя обычно для этого используется другой метод — CreateNew: один из процессов создает блок разделяемой памяти, вызывая MemoryMappedFile.CreateNew и передает ему имя создаваемого размещенного в памяти файла, другой процесс подписывается на этот блок памяти, вызывая метод MemoryMappedFile.OpenExisting с таким же именем.
1 2 3 4 5 6 7 8 9 10 11 | // Первый процесс: using (MemoryMappedFile mmFile = MemoryMappedFile.CreateNew ("Demo", 500)) using (MemoryMappedViewAccessor accessor = mmFile.CreateViewAccessor()) { accessor.Write (0, 12345); Console.ReadLine(); } // Второй процесс: using (MemoryMappedFile mmFile = MemoryMappedFile.OpenExisting ("Demo")) using (MemoryMappedViewAccessor accessor = mmFile.CreateViewAccessor()) Console.WriteLine (accessor.ReadInt32 (0)); // 12345 |
Методы аксессора представления
Метод CreateViewAccessor объекта MemoryMappedFile создает аксессор представления — объект MemoryMappedViewAccessor, позволяющий выполнять чтение и запись в произвольных позициях.
Методы Read*/Write* принимают числовые типы, bool, char, массивы и структуры (содержащие элементы и поля значимых типов). Ссылочные типы и их массивы/структуры запрещены. Поэтому чтобы записать строку ее нужно закодировать в байтовый массив:
1 2 3 4 5 6 7 8 | // Запись строки byte[] data = Encoding.UTF8.GetBytes ("This is a test"); accessor.Write (0, data.Length); accessor.WriteArray (4, data, 0, data.Length); // Чтение строки byte[] data = new byte [accessor.ReadInt32 (0)]; accessor.ReadArray (4, data, 0, data.Length); Console.WriteLine (Encoding.UTF8.GetString (data)); // This is a test |
Изолированное хранилище
Каждая программа .NET имеет доступ к локальной области хранения, уникальной для этой программы, которая называется изолированным хранилищем. Это хранилище удобно, когда программа не имеет доступа к стандартной файловой системе (приложения Silverlight и некоторые интернет приложения), но его использование обладает рядом недостатков (неудобство работы, большие ограничения).
Чтобы получить поток изолированного хранилища, сначала нужно указать требуемый тип изоляции, вызвав один из статических методов класса IsolatedStorageFile:
GetUserStoreForDomainGetMachineStoreForDomainGetUserStoreForAssemblyGetMachineStoreForAssembly
Затем он используется для создания объекта IsolatedStorageFileStream:
1 2 3 4 5 6 7 8 | using (IsolatedStorageFile f = IsolatedStorageFile.GetMachineStoreForDomain()) using (var s = new IsolatedStorageFileStream ("hi.txt",FileMode.Create,f)) using (var writer = new StreamWriter (s)) writer.WriteLine ("Hello, World"); using (IsolatedStorageFile f = IsolatedStorageFile.GetMachineStoreForDomain()) using (var s = new IsolatedStorageFileStream ("hi.txt", FileMode.Open, f)) using (var reader = new StreamReader (s)) Console.WriteLine (reader.ReadToEnd()); |
IsolatedStorageFile вызвав статический метод IsolatedStorageFile.GetStore, передав ему правильную комбинацию флагов StorageScope:1 2 3 4 5 6 7 | var flags = IsolatedStorageScope.Machine | IsolatedStorageScope.Application | IsolatedStorageScope.Assembly; using (IsolatedStorageFile f = IsolatedStorageFile.GetStore (flags, typeof (StrongName), typeof (StrongName))) { ... |

