龙空技术网

c# 10 教程:15 流和 I/O

启辰8 65

前言:

此刻各位老铁们对“ubuntuacl设置删除权限”大体比较关注,姐妹们都想要剖析一些“ubuntuacl设置删除权限”的相关文章。那么小编同时在网摘上网罗了一些关于“ubuntuacl设置删除权限””的相关内容,希望咱们能喜欢,你们一起来学习一下吧!

本章介绍 .NET 中输入和输出的基本类型,重点介绍以下主题:

.NET 流体系结构及其如何为跨各种 I/O 类型的读取和写入提供一致的编程接口用于操作磁盘上的文件和目录的类用于压缩、命名管道和内存映射文件的专用流。

本章重点介绍 System.IO 命名空间(较低级别 I/O 功能的所在地)中的类型。

流体系结构

.NET 流体系结构以三个概念为中心:后备存储、修饰器和适配器,如图 所示。

是使输入和输出有用的终结点,例如文件或网络连接。准确地说,它是以下一种或两种:

可从中按顺序读取字节的源可以按顺序写入字节的目标

但是,除非向程序员公开,否则后备存储是没有用的。流是用于此目的的标准 .NET 类;它公开了一组用于读取、写入和定位的标准方法。与所有后备数据同时存在于内存中的数组不同,流以串行方式处理数据 — 一次处理一个字节或以可管理大小的块处理数据。因此,流可以使用少量的固定内存,而不管其后备存储的大小如何。

流分为两类:

支持存储流

这些硬连接到特定类型的后备存储,例如 文件流 或 网络流 。

装饰器流

这些馈送来自另一个流,以某种方式转换数据,例如 放气流 or 加密流 .

修饰器流具有以下体系结构优势:

它们将后备存储流从需要自己实现压缩和加密等功能中解放出来。流在装饰时不会发生界面更改。在运行时连接装饰器。您可以将装饰器链接在一起(例如,压缩器后跟加密)。

后备存储流和装饰器流都以字节为单位专门处理。尽管这既灵活又高效,但应用程序通常在更高级别(如文本或 XML)上工作。通过使用类型化为特定格式的专用方法将流包装在类中来弥合此差距。例如,文本读取器公开 ReadLine 方法;XML 编写器公开 WriteAttributes 方法。

注意

适配器包装流,就像装饰器一样。但是,与装饰器不同,适配器不是流;它通常完全隐藏面向字节的方法。

总而言之,后备存储流提供原始数据;装饰器流提供透明的二进制转换,例如加密;适配器提供类型化方法,用于处理更高级别的类型(如字符串和 XML)。 说明了它们的关联。要组成链,只需将一个对象传递到另一个对象的构造函数中即可。

使用流

抽象 Stream 类是所有流的基础。它定义了三个基本操作的方法和属性:、和,以及关闭、刷新和配置超时等管理任务(请参阅)。

流类成员

类别

成员

读数

公共摘要布尔 CanRead { get; }

公共抽象 int 读取 (byte[] 缓冲区, int 偏移量, int count)

public virtual int ReadByte();

写作

public abstract bool CanWrite { get; }

公共抽象 void 写入 (byte[] buffer, int offset, int count);

公共虚拟空写字节(字节值);

寻求

public abstract bool CanSeek { get; }

公共摘要多头仓位 { get; set; }

公共抽象空集长度(长值);

公共摘要长长度 { 得到; }

公共摘要长寻道(长偏移,寻源原点);

关闭/冲洗

公共虚拟无效 关闭();

公共无效处置();

公共抽象无效同花顺();

超时

公共虚拟布尔值 CanTimeout { get; }

public virtual int ReadTimeout { get; set; }

public virtual int WriteTimeout { get; set; }

其他

公共静态只读流空;“空”流

公共静态流同步(流流);

还有异步版本的读取和写入方法,它们都返回 Task s 并选择性地接受取消令牌,以及处理 Span<T> 和 Memory<T> 类型的重载,我们在中描述。

在以下示例中,我们使用文件流来读取、写入和查找:

using System;using System.IO;// Create a file called test.txt in the current directory: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);     // Write block of 5 bytes  Console.WriteLine (s.Length);         // 7  Console.WriteLine (s.Position);       // 7  s.Position = 0;                       // Move back to the start  Console.WriteLine (s.ReadByte());     // 101  Console.WriteLine (s.ReadByte());     // 102  // Read from the stream back into the block array:  Console.WriteLine (s.Read (block, 0, block.Length));   // 5  // Assuming the last Read returned 5, we'll be at  // the end of the file, so Read will now return 0:  Console.WriteLine (s.Read (block, 0, block.Length));   // 0}

异步读取或写入只是调用 ReadAsync / WriteAsync 而不是 读/写 的问题,并等待表达式(我们还必须将 async 关键字添加到调用方法中,如所述):

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);    // Write asychronously    s.Position = 0;                       // Move back to the start    // Read from the stream back into the block array:    Console.WriteLine (await s.ReadAsync (block, 0, block.Length));   // 5  }}

异步方法可以轻松编写响应迅速且可缩放的应用程序,这些应用程序可以处理可能较慢的流(尤其是网络流),而不会占用线程。

注意

为了简洁起见,我们将继续对本章中的大多数示例使用同步方法;但是,我们建议在涉及网络 I/O 的大多数方案中最好使用异步读/写操作。

阅读和写作

流可以支持读取和/或写入。如果 CanWrite 返回 false ,则流是只读的;如果 CanRead 返回 false ,则流是只写的。

读取将流中的数据块接收到数组中。它返回接收的字节数,该字节数始终小于或等于 count 参数。如果它小于计数,则表示流的末尾已到达,或者流以较小的块形式为您提供数据(网络流通常就是这种情况)。无论哪种情况,数组中的字节余额都将保持未写入状态,并保留其先前的值。

注意

使用 Read ,只有当该方法返回 0 时,您才能确定您已到达流的末尾。因此,如果你有一个 1,000 字节的流,以下代码可能无法将其全部读入内存:

// Assuming s is a stream:byte[] data = new byte [1000];s.Read (data, 0, data.Length);

Read 方法可以读取 1 到 1,000 个字节之间的任何内容,使流的余额保持未读取状态。

以下是读取 1,000 字节流的正确方法:

byte[] data = new byte [1000];// bytesRead will always end up at 1000, unless the stream is// itself smaller in length:int bytesRead = 0;int chunkSize = 1;while (bytesRead < data.Length && chunkSize > 0)  bytesRead +=    chunkSize = s.Read (data, bytesRead, data.Length - bytesRead);
注意

幸运的是,BinaryReader 类型提供了一种更简单的方法来实现相同的结果:

byte[] data = new BinaryReader (s).ReadBytes (1000);

如果流的长度小于 1,000 字节,则返回的字节数组将反映实际的流大小。如果流是可搜索的,您可以通过将 1000 替换为 (int)s.Length 来读取其全部内容。

我们在中进一步描述了 BinaryReader 类型。

ReadByte 方法更简单:它只读取一个字节,返回 -1 以指示流的结束。ReadByte 实际上返回一个 int 而不是一个字节,因为后者不能返回 −1。

Write 和 WriteByte 方法将数据发送到流。如果它们无法发送指定的字节,则会引发异常。

注意

在 Read 和 Write 方法中,offset 参数引用缓冲区数组中开始读取或写入的索引,而不是流中的位置。

寻求

如果 CanSeek 返回 true ,则流是可搜索的。使用可搜索的流(例如文件流),您可以查询或修改其长度(通过调用 SetLength ),并随时更改正在读取或写入的位置。属性相对于流的开头;但是,Seek 方法允许您相对于流的当前位置或末尾移动。

注意

更改文件流上的位置通常需要几微秒。如果你在一个循环中执行数百万次,MemoryMappedFile 类可能是比 FileStream 更好的选择(请参阅)。

对于不可搜索的流(例如加密流),确定其长度的唯一方法是完全读取它。此外,如果您需要重读上一节,则必须关闭流并从新流重新开始。

关闭和冲洗

流必须在使用后释放,以释放基础资源,例如文件和套接字句柄。保证这一点的一种简单方法是使用块实例化流。通常,流遵循标准处置语义:

“处置”和“关闭”在功能上是相同的。重复释放或关闭流不会导致错误。

关闭修饰器流会关闭装饰器及其后备存储流。对于装饰器链,关闭最外层的装饰器(在链的头部)会关闭整个批次。

某些流在内部缓冲进出后备存储的数据,以减少往返,从而提高性能(文件流就是一个很好的例子)。这意味着写入流的数据可能不会立即到达后备存储;当缓冲区填满时,它可能会延迟。Flush 方法强制立即写入任何内部缓冲数据。关闭流时会自动调用刷新,因此您无需执行以下操作:

s.Flush(); s.Close();
超时

流支持读取和写入超时,如果 CanTimeout 返回 true。网络流支持超时;文件和内存流则不然。对于支持超时的流,“读取超时”和“写入超时”属性确定所需的超时(以毫秒为单位),其中 0 表示没有超时。读取和写入方法通过引发异常来指示发生超时。

异步读取异步/写入异步方法不支持超时;相反,您可以将取消令牌传递到这些方法中。

线程安全

通常,流不是线程安全的,这意味着两个线程不能同时读取或写入同一流而不会出错。Stream 类通过静态同步方法提供了一个简单的解决方法。此方法接受任何类型的流并返回线程安全包装器。包装器的工作原理是在每个读取、写入或查找周围获取独占锁,确保一次只有一个线程可以执行此类操作。实际上,这允许多个线程同时将数据追加到同一流 - 其他类型的活动(如并发读取)需要额外的锁定,以确保每个线程访问流的所需部分。我们将在第 中全面讨论线程安全性。

注意

从 .NET 6 开始,可以使用 RandomAccess 类执行高性能线程安全文件 I/O 操作。随机访问还允许您传入多个缓冲区以提高性能。

支持存储流

显示了 .NET 提供的密钥后备存储流。“空流”也可以通过流的静态空字段获得。空流在编写单元测试时很有用。

在以下部分中,我们将描述 文件流 和 内存流 ;在本章的最后一节中,我们描述了 隔离存储流 。在第中,我们将介绍 网络流 。

文件流

在本节前面,我们演示了 FileStream 读取和写入数据字节的基本用法。现在让我们检查一下此类的特殊功能。

注意

如果您使用的是通用 Windows 平台 (UWP),您还可以使用 Windows.Storage 中的类型执行文件 I/O。我们在 的在线增刊中对此进行了描述。

构造文件流

实例化 FileStream 的最简单方法是在 File 类上使用以下静态外观方法之一:

FileStream fs1 = File.OpenRead  ("readme.bin");            // Read-onlyFileStream fs2 = File.OpenWrite ("writeme.tmp");           // Write-onlyFileStream fs3 = File.Create    ("readwrite.tmp");         // Read/write

如果文件已存在,则 OpenWrite 和 Create 的行为会有所不同。创建截断任何现有内容;OpenWrite 保留现有内容不变,流定位为零。如果您写入的字节数比以前在文件中的字节少,OpenWrite 会混合使用新旧内容。

您也可以直接实例化 文件流 。它的构造函数提供对每个功能的访问,允许您指定文件名或低级文件句柄、文件创建和访问模式以及用于共享、缓冲和安全性的选项。下面打开一个现有文件进行读/写访问,而不会覆盖它(using 关键字确保在 fs 退出作用域时释放它):

using var fs = new FileStream ("readwrite.tmp", FileMode.Open);

我们稍后会更仔细地研究文件模式。

指定文件名

文件名可以是绝对的(例如,,或者在Unix中,)或相对于当前目录(例如,或)。可以通过静态 Environment.CurrentDirectory 访问或更改当前目录。

注意

当程序启动时,当前目录可能与程序可执行文件的目录一致,也可能不一致。因此,切勿依赖当前目录来查找与可执行文件一起打包的其他运行时文件。

AppDomain.CurrentDomain.BaseDirectory 返回目录,在正常情况下,该目录是包含程序可执行文件的文件夹。要指定相对于此目录的文件名,可以调用 Path.Comb:

string baseFolder = AppDomain.CurrentDomain.BaseDirectory;string logoPath = Path.Combine (baseFolder, "logo.jpg");Console.WriteLine (File.Exists (logoPath));
文件类上的快捷方式方法

以下静态方法在一个步骤中将整个文件读入内存:

File.ReadAllText (返回一个字符串)File.ReadAllLines(返回字符串数组)File.ReadAllBytes(返回一个字节数组)

以下静态方法一步写入整个文件:

文件.全部写入文本File.WriteAllLinesFile.WriteAllBytesFile.AppendAllText(非常适合附加到日志文件)

还有一个名为File.ReadLines的静态方法:这类似于ReadAllLines,只是它返回一个延迟计算的IEnumerable<string>。这更有效,因为它不会一次将整个文件加载到内存中。LINQ 非常适合使用结果;下面计算长度大于 80 个字符的行数:

int longLines = File.ReadLines ("filePath")                    .Count (l => l.Length > 80);

您可以通过通用命名约定 (UNC) 路径(如 \\\pic.jpg 或 \\)跨 Windows 网络进行读写。(若要从 macOS 或 Unix 访问 Windows 文件共享,请按照特定于操作系统的说明将其挂载到文件系统,然后使用 C# 中的普通路径打开它。

指定文件模式

所有接受文件名的 FileStream 构造函数也需要 FileMode 枚举参数。 显示了如何选择 FileMode,这些选择产生的结果类似于在 File 类上调用静态方法。

注意

File.Create 和 FileMode.Create 如果在隐藏文件上使用,将引发异常。要覆盖隐藏文件,必须删除并重新创建它:

File.Delete ("hidden.txt");using var file = File.Create ("hidden.txt");...

仅使用文件名和文件模式构造文件流会为您提供(只有一个例外)可读/可写流。如果您还提供了 FileAccess 参数,则可以请求降级:

[Flags]public enum FileAccess { Read = 1, Write = 2, ReadWrite = 3 }

下面返回一个只读流,等效于调用 File.OpenRead:

using var fs = new FileStream ("x.bin", FileMode.Open, FileAccess.Read);...

FileMode.Append是一个奇怪的:使用这种模式,你会得到一个流。若要追加读写支持,必须改为使用 FileMode.Open 或 FileMode.OpenOrCreate,然后查找流的末尾:

using var fs = new FileStream ("myFile.bin", FileMode.Open);fs.Seek (0, SeekOrigin.End);...
高级文件流功能

以下是构造文件流时可以包含的其他可选参数:

一个文件共享枚举,描述在您完成之前授予想要浸入同一文件的其他进程多少访问权限(无、读取 [默认]、读写或写入)。内部缓冲区的大小(以字节为单位)(当前默认值为 4 KB)。指示是否遵从操作系统进行异步 I/O 的标志。FileOptions 标记枚举,用于请求操作系统加密(加密)、关闭临时文件时自动删除 (DeleteOnClose ) 和优化提示(随机访问和顺序扫描)。还有一个直写标志,要求操作系统禁用后写缓存;这适用于事务文件或日志。基础操作系统不支持的标志将被静默忽略。

使用 FileShare.ReadWrite 打开文件允许其他进程或用户同时读取和写入同一文件。为了避免混乱,你们都可以同意在读取或写入之前锁定文件的指定部分,使用以下方法:

// Defined on the FileStream class:public virtual void Lock   (long position, long length);public virtual void Unlock (long position, long length);

如果请求的部分或全部文件部分已被锁定,则 Lock 将引发异常。

内存流

MemoryStream 使用数组作为后备存储。这部分违背了拥有流的目的,因为整个后备存储必须立即驻留在内存中。当您需要随机访问不可搜索的流时,MemoryStream 仍然很有用。如果您知道源流的大小可管理,则可以将其复制到内存流中,如下所示:

var ms = new MemoryStream();sourceStream.CopyTo (ms);

您可以通过调用 ToArray 将 MemoryStream 转换为字节数组。GetBuffer 方法通过返回对基础存储阵列的直接引用来更有效地完成相同的工作;不幸的是,这个数组通常比流的实际长度长。

注意

关闭和刷新内存流是可选的。如果关闭 MemoryStream ,则无法再读取或写入它,但仍允许调用 ToArray 以获取底层数据。刷新对内存流绝对不执行任何操作。

您可以在第 的”和“中找到更多 MemoryStream 示例。

管道流

PipeStream 提供了一种简单的方法,通过这种方法,一个进程可以通过操作系统的协议与另一个进程进行通信。管道有两种:

匿名管道(更快)

允许同一台计算机上的父进程和子进程之间进行单向通信

命名管道(更灵活)

允许网络上的任意进程或不同计算机上的任意进程之间进行双向通信

管道适用于单台计算机上的进程间通信 (IPC):它不依赖于网络传输,这意味着没有网络协议开销,并且防火墙没有问题。

注意

管道是基于流的,因此一个进程等待接收一系列字节,而另一个进程发送它们。另一种方法是让进程通过共享内存块进行通信;我们在中描述了如何执行此操作。

PipeStream 是一个抽象类,具有四个具体的子类型。两个用于匿名管道,另外两个用于命名管道:

匿名管道

AnonymousPipeServerStream 和 AnonymousPipeClientStream

命名管道

NamedPipeServerStream 和 NamedPipeClientStream

命名管道更易于使用,因此我们首先介绍它们。

命名管道

使用命名管道,各方通过同名管道进行通信。该协议定义了两个不同的角色:客户端和服务器。客户端和服务器之间的通信发生如下:

服务器实例化 NamedPipeServerStream,然后调用 WaitForConnection 。客户端实例化 NamedPipeClientStream,然后调用 Connect(具有可选的超时)。

然后,双方读取和写入流以进行通信。

下面的示例演示发送单个字节 (100) 然后等待接收单个字节的服务器:

using var s = new NamedPipeServerStream ("pipedream");s.WaitForConnection();s.WriteByte (100);                // Send the value 100.Console.WriteLine (s.ReadByte());

下面是相应的客户端代码:

using var s = new NamedPipeClientStream ("pipedream");s.Connect();Console.WriteLine (s.ReadByte());s.WriteByte (200);                 // Send the value 200 back.

默认情况下,命名管道流是双向的,因此任何一方都可以读取或写入其流。这意味着客户端和服务器必须就某些协议达成一致来协调他们的操作,这样双方就不会同时发送或接收。

还需要就每次传输的长度达成一致。在这方面,我们的例子是微不足道的,因为我们在每个方向上只反弹了一个字节。为了帮助处理长度超过一个字节的消息,管道提供了传输模式(仅限 Windows)。如果启用此功能,则调用 Read 的一方可以通过检查 IsMessageComplete 属性来知道消息何时完成。为了演示,我们首先编写一个帮助程序方法,该方法从启用消息的 PipeStream 读取整条消息 — 换句话说,读取直到 IsMessageComplete 为真:

static byte[] ReadMessage (PipeStream s){  MemoryStream ms = new MemoryStream();  byte[] buffer = new byte [0x1000];      // Read in 4 KB blocks  do    { ms.Write (buffer, 0, s.Read (buffer, 0, buffer.Length)); }  while (!s.IsMessageComplete);  return ms.ToArray();}

(要使此异步,请将“s.Read ”替换为“await s.ReadAsync ”。

注意

您无法仅通过等待 Read 返回 0 来确定 PipeStream 是否已完成消息的读取。这是因为,与大多数其他流类型不同,管道流和网络流没有明确的终点。相反,它们在消息传输之间暂时“干涸”。

现在我们可以激活消息传输模式。在服务器上,这是通过在构造流时指定 PipeTransmission Mode.Message 来完成的:

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)));

在客户端上,我们通过在调用 Connect 后设置 ReadMode 来激活消息传输模式:

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);
注意

消息模式仅在 Windows 上受支持。其他平台抛出 PlatformNotSupportedException 。

匿名管道

匿名管道在父进程和子进程之间提供单向通信流。匿名管道不使用系统范围的名称,而是通过专用句柄进行调入。

与命名管道一样,存在不同的客户端和服务器角色。但是,通信系统略有不同,其过程如下:

服务器实例化一个匿名管道服务器流,提交到In或Out的PipeDirection。服务器调用 GetClientHandleAsString 来获取管道的标识符,然后将其传递给客户端(通常在启动子进程时作为参数)。子进程实例化一个 AnonymousPipeClientStream,指定相反的 PipeDirection。服务器通过调用 DisposeLocalCopyOfClientHandle 释放在步骤 2 中生成的本地句柄。父进程和子进程通过阅读/写流进行通信。

由于匿名管道是单向的,因此服务器必须创建两个管道进行双向通信。下面的Console程序创建两个管道(输入和输出),然后启动一个子进程。然后,它向子进程发送一个字节,并接收一个字节作为返回:

class Program{  static void Main (string[] args)  {    if (args.Length == 0)      // No arguments signals server mode      AnonymousPipeServer();    else      // We pass in the pipe handle IDs as arguments to signal client mode      AnonymousPipeClient (args [0], args [1]);  }  static void AnonymousPipeClient (string rxID, string txID)  {    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);    }  }  static void AnonymousPipeServer ()  {    using var tx = new AnonymousPipeServerStream (                     PipeDirection.Out, HandleInheritability.Inheritable);    using var rx = new AnonymousPipeServerStream (                     PipeDirection.In, HandleInheritability.Inheritable);    string txID = tx.GetClientHandleAsString ();    string rxID = rx.GetClientHandleAsString ();    // Create and start up a child process.    // We'll use the same Console executable, but pass in arguments:    string thisAssembly = Assembly.GetEntryAssembly().Location;    string thisExe = Path.ChangeExtension (thisAssembly, ".exe");    var args = $"{txID} {rxID}";    var startInfo = new ProcessStartInfo (thisExe, args);    startInfo.UseShellExecute = false;       // Required for child process    Process p = Process.Start (startInfo);    tx.DisposeLocalCopyOfClientHandle ();    // Release unmanaged    rx.DisposeLocalCopyOfClientHandle ();    // handle resources.    tx.WriteByte (100);    // Send a byte to the child process    Console.WriteLine ("Server received: " + rx.ReadByte ());    p.WaitForExit ();  }}

与命名管道一样,客户端和服务器必须协调它们的发送和接收,并就每次传输的长度达成一致。遗憾的是,匿名管道不支持消息模式,因此您必须实现自己的消息长度协议协议。一种解决方案是在每次传输的前四个字节中发送一个整数值,该值定义要遵循的消息的长度。类提供了在整数和四个字节的数组之间进行转换的方法。

缓冲流

BufferedStream 装饰或包装具有缓冲功能的另一个流,它是 .NET 中的许多修饰器流类型之一,所有这些类型如图 所示。

缓冲通过减少到后备存储的往返次数来提高性能。以下是我们如何将文件流包装在 20 KB 的缓冲流中:

// Write 100K to a file:File.WriteAllBytes ("myFile.bin", new byte [100000]);using FileStream fs = File.OpenRead ("myFile.bin");using BufferedStream bs = new BufferedStream (fs, 20000);  //20K bufferbs.ReadByte();Console.WriteLine (fs.Position);         // 20000

在此示例中,由于预读缓冲,基础流在仅读取一个字节后前进 20,000 字节。我们可以在FileStream再次被击中之前再调用ReadByte19,999次。

将 BufferedStream 耦合到 FileStream (如本例所示)的价值有限,因为 FileStream 已经具有内置的缓冲功能。它的唯一用途可能是扩大已经构建的文件流上的缓冲区。

关闭缓冲流会自动关闭基础后备存储流。

流适配器

流仅以字节为单位处理;若要读取或写入字符串、整数或 XML 元素等数据类型,必须插入适配器。下面是 .NET 提供的功能:

文本适配器(用于字符串和字符数据)

文本阅读器 , 文本编写器

StreamReader , StreamWriter

StringReader , StringWriter

二进制适配器(用于基元类型,如 int、bool、string 和 float)

BinaryReader , BinaryWriter

XML 适配器(中介绍))

XmlReader , XmlWriter

说明了这些类型之间的关系。

文本适配器

TextReader 和 TextWriter 是专门处理字符和字符串的适配器的抽象基类。每个在 .NET 中都有两个常规用途实现:

流阅读器 / 流写入器

使用 Stream 作为其原始数据存储,将流的字节转换为字符或字符串

StringReader / StringWriter

使用内存中的字符串实现文本阅读器/文本编写器

按类别列出了 TextReader 的成员。Peek 返回流中的下一个字符,而不前进位置。如果流的末尾,Peek 和 Read 的零参数版本都返回 −1;否则,它们返回一个可以直接转换为 char 的整数。接受 char[] 缓冲区的 Read 重载在功能上与 ReadBlock 方法相同。ReadLine 读取,直到依次到达 CR(字符 13)或 LF(字符 10)或 CR+LF 对。然后,它返回一个字符串,丢弃 CR/LF 字符。

文本阅读器成员

类别

成员

读取一个字符

public virtual int Peek();将结果转换为字符

公共虚拟 int 读取();将结果转换为字符

读取许多字符

公共虚拟 int 读取 (char[] 缓冲区, int index, int count);

public virtual int ReadBlock (char[] buffer, int index, int count);

公共虚拟字符串 ReadLine();

公共虚拟字符串 ReadToEnd();

关闭

公共虚拟无效 关闭();

公共无效处置();与关闭相同

其他

公共静态只读文本读取器空;

公共静态文本阅读器同步(文本阅读器阅读器);

注意

Environment.NewLine 返回当前操作系统的换行符序列。

在Windows上,这是“\r\n”(想想“ReturN”),并且松散地模仿机械打字机:CR(字符13)后跟LF(字符10)。颠倒顺序,你会得到两个新行或没有!

在 Unix 和 macOS 上,它只是 “\n” .

TextWriter具有类似的写入方法,如所示。此外,还会重载 Write 和 WriteLine 方法,以接受每个基元类型以及对象类型。这些方法只是在传入的任何内容上调用 ToString 方法(可选地通过调用方法或构造 TextWriter 时指定的 IFormatProvider)。

文本编写器成员

类别

成员

写一个字符

公共虚拟空 写入(字符值);

编写许多字符

公共虚拟空 写入(字符串值);

public virtual void Write (char[] buffer, int index, int count);

public virtual void Write (string format, params object[] arg);

公共虚拟空写行(字符串值);

关闭和冲洗

公共虚拟无效 关闭();

公共无效处置();与关闭相同

公共虚拟虚空同花顺();

格式化和编码

public virtual IFormatProvider FormatProvider { get; }

公共虚拟字符串换行符 { get; set; }

公共摘要编码编码 { get; }

其他

公共静态只读文本编写器空;

公共静态文本编写器同步(文本编写器编写器);

WriteLine 只是将给定的文本附加到 Environment.NewLine 。您可以通过 NewLine 属性更改此设置(这对于与 Unix 文件格式的互操作性非常有用)。

注意

与Stream一样,TextReader和TextWriter提供了基于任务的异步版本的读/写方法。

StreamReader 和 StreamWriter

在下面的示例中,StreamWriter 将两行文本写入文件,然后 StreamReader 读回该文件:

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());       // Line1  Console.WriteLine (reader.ReadLine());       // Line2}

由于文本适配器经常与文件耦合,因此 File 类提供了静态方法 CreateText 、AppendText 和 OpenText 来缩短该过程:

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());     // Line1                                               // Line2                                               // Line3

这也说明了如何测试文件的结尾(即读取器。躲猫猫() )。另一种选择是阅读直到阅读器。读取行返回空值。

您还可以读取和写入其他类型的类型(如整数),但由于 TextWriter 在您的类型上调用 ToString,因此在读回字符串时必须对其进行分析:

using (TextWriter w = File.CreateText ("data.txt")){  w.WriteLine (123);          // Writes "123"  w.WriteLine (true);         // Writes the word "true"}using (TextReader r = File.OpenText ("data.txt")){  int myInt = int.Parse (r.ReadLine());     // myInt == 123  bool yes = bool.Parse (r.ReadLine());     // yes == true}
字符编码

TextReader 和 TextWriter 本身只是抽象类,与流或后备存储没有连接。但是,StreamReader 和 StreamWriter 类型连接到面向字节的基础流,因此它们必须在字符和字节之间进行转换。它们通过 命名空间中的编码类执行此操作,该类可在构造 StreamReader 或 StreamWriter 时选择。如果选择“无”,则使用默认的 UTF-8 编码。

注意

如果显式指定编码,默认情况下,StreamWriter 会将前缀写入流的开头以标识编码。这通常是不可取的,您可以通过按如下方式构造编码来防止它:

var encoding = new UTF8Encoding (  encoderShouldEmitUTF8Identifier:false,  throwOnInvalidBytes:true);

第二个参数告诉 StreamWriter(或 StreamReader )在遇到没有有效字符串转换的字节时引发异常,如果未指定编码,则与其默认行为匹配。

最简单的编码是 ASCII,因为每个字符由一个字节表示。ASCII 编码将 Unicode 集的前 127 个字符映射到其单个字节中,涵盖您在美式键盘上看到的内容。大多数其他字符(包括专用符号和非英语字符)无法表示,并转换为□字符。默认的 UTF-8 编码可以映射所有分配的 Unicode 字符,但它更复杂。前 127 个字符编码为单个字节,以实现 ASCII 兼容性;其余字符编码为可变数量的字节(最常见的是两个或三个)。请考虑以下事项:

using (TextWriter w = File.CreateText ("but.txt"))    // Use default UTF-8  w.WriteLine ("but-");                               // encoding.using (Stream s = File.OpenRead ("but.txt"))  for (int b; (b = s.ReadByte()) > -1;)    Console.WriteLine (b);

单词“but”后面不是股票标准连字符,而是更长的长破折号 (—) 字符 U+2014。这是不会给您的图书编辑器带来麻烦的!让我们检查一下输出:

98     // b117    // u116    // t226    // em dash byte 1       Note that the byte values128    // em dash byte 2       are >= 128 for each part148    // em dash byte 3       of the multibyte sequence.13     // <CR>10     // <LF>

由于长破折号位于 Unicode 集的前 127 个字符之外,因此以 UTF-8 编码需要多个字节(在本例中为三个)。UTF-8 对西方字母表很有效,因为大多数常用字符只占用一个字节。它还可以通过忽略 127 以上的所有字节轻松降级到 ASCII。它的缺点是在流中查找很麻烦,因为字符的位置与其在流中的字节位置不对应。另一种方法是 UTF-16(在编码类中仅标记为“Unicode”)。以下是我们如何用 UTF-16 编写相同的字符串:

using (Stream s = File.Create ("but.txt"))using (TextWriter w = new StreamWriter (s, Encoding.Unicode))  w.WriteLine ("but-");foreach (byte b in File.ReadAllBytes ("but.txt"))  Console.WriteLine (b);

这是输出:

255    // Byte-order mark 1254    // Byte-order mark 298     // 'b' byte 10      // 'b' byte 2117    // 'u' byte 10      // 'u' byte 2116    // 't' byte 10      // 't' byte 220     // '--' byte 132     // '--' byte 213     // <CR> byte 10      // <CR> byte 210     // <LF> byte 10      // <LF> byte 2

从技术上讲,UTF-16 每个字符使用两个或四个字节(分配或保留了近一百万个 Unicode 字符,因此两个字节并不总是足够的)。但是,由于 C# 字符类型本身只有 16 位宽,因此 UTF-16 编码将始终为每个 .NET 字符使用两个字节。这样可以轻松跳转到流中的特定字符索引。

UTF-16 使用双字节前缀来标识字节对是按“小端序”还是“大端序”顺序(最低有效字节在前还是最高有效字节在前)写入。默认的小端顺序是基于 Windows 的系统的标准顺序。

StringReader 和 StringWriter

StringReader 和 StringWriter 适配器根本不包装流;相反,它们使用字符串或 StringBuilder 作为基础数据源。这意味着不需要字节转换 — 事实上,这些类不会执行任何您无法通过字符串或 StringBuilder 与索引变量结合使用来实现的操作。不过,它们的优势在于它们与StreamReader / StreamWriter共享一个基类。例如,假设我们有一个包含XML的字符串,并希望使用XmlReader解析它。方法接受下列方法之一:

一个 URI一个流文本阅读器

那么,我们如何对字符串进行XML解析呢?因为StringReader是TextReader的一个子类,我们很幸运。我们可以实例化并传入一个 StringReader,如下所示:

XmlReader r = XmlReader.Create (new StringReader (myString));
二进制适配器

BinaryReader 和 BinaryWriter 读写原生数据类型:布尔、字节、字符、十进制、浮点数、双精度、短精度、整数、长整型、字节数、ushort 、uint 和 ulong,以及原始数据类型的字符串和数组。

与StreamReader和StreamWriter不同,二进制适配器有效地存储基元数据类型,因为它们在内存中表示。因此,int 使用四个字节;双精度使用八个字节。字符串是通过文本编码写入的(如StreamReader和StreamWriter),但以长度为前缀,以便可以在不需要特殊分隔符的情况下读回一系列字符串。

假设我们有一个简单的类型,定义如下:

public class Person{  public string Name;  public int    Age;  public double Height;}

我们可以将以下方法添加到 Person 中,以使用二进制适配器将其数据保存到/从流中加载:

public void SaveData (Stream s){  var w = new BinaryWriter (s);  w.Write (Name);  w.Write (Age);  w.Write (Height);  w.Flush();         // Ensure the BinaryWriter buffer is cleared.                     // We won't dispose/close it, so more data}                    // can be written to the stream.public void LoadData (Stream s){  var r = new BinaryReader (s);  Name   = r.ReadString();  Age    = r.ReadInt32();  Height = r.ReadDouble();}

BinaryReader 还可以读取字节数组。下面读取可搜索流的全部内容:

byte[] data = new BinaryReader (s).ReadBytes ((int) s.Length);

这比直接从流中读取更方便,因为它不需要循环来确保已读取所有数据。

关闭和释放流适配器

拆卸下游适配器时,您有四种选择:

仅关闭适配器。关闭适配器,然后关闭流。(作家)刷新适配器,然后关闭流。(致读者)仅关闭流。注意

关闭和处置是适配器的同义词,就像它们与流一样。

选项 1 和 2 在语义上是相同的,因为关闭适配器会自动关闭基础流。每当使用语句嵌套时,您都会隐式采用选项 2:

using (FileStream fs = File.Create ("test.txt"))using (TextWriter writer = new StreamWriter (fs))  writer.WriteLine ("Line");

由于嵌套从内向外处置,因此首先关闭适配器,然后关闭流。此外,如果在适配器的构造函数中引发异常,流仍会关闭。嵌套的 using 语句很难出错!

注意

切勿在关闭或刷新流的写入器之前关闭流 - 您将截断适配器中缓冲的任何数据。

选项 3 和 4 之所以有效,是因为适配器属于一次性对象的不寻常类别。您可能选择不释放适配器的一个示例是,当您完成适配器,但您希望将基础流保持打开状态以供后续使用时:

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());}

在这里,我们写入文件,重新定位流,然后在关闭流之前读取第一个字节。如果我们处置了 StreamWriter,它也会关闭底层的 FileStream,导致后续读取失败。附带条件是我们调用 Flush 来确保 StreamWriter 的缓冲区被写入底层流。

注意

流适配器(及其可选的处置语义)不实现终结器调用 Dispose 的扩展处置模式。这允许废弃的适配器在垃圾回收器赶上时逃避自动处置。

StreamReader / StreamWriter上还有一个构造函数,指示它在处置后保持流打开。因此,我们可以重写前面的示例,如下所示:

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 。两者都使用类似于 ZIP 格式的流行压缩算法。它们的不同之处在于GZipStream在开始和结束时编写了一个额外的协议 - 包括一个CRC来检测错误。GZipStream还符合其他软件认可的标准。

.NET还包括BrotliStream,它实现了压缩算法。BrotliStream比DeflateStream和GZipStream慢10倍以上,但实现了更好的压缩比。(性能影响仅适用于压缩 — 解压缩性能非常好。

所有三个流都允许读取和写入,但有以下条件:

压缩流。解压缩时,您始终从流。

DeflateStream、GZipStream 和 BrotliStream 是装饰器;它们压缩或解压缩您在构造中提供的另一个流中的数据。在下面的示例中,我们使用 FileStream 作为后备存储来压缩和解压缩一系列字节:

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());     // Writes 0 to 99

使用DeflateStream,压缩文件是102字节:比原始文件略大(BrotliStream会将其压缩为73字节)。压缩对于“密集”、非重复的二进制数据效果不佳(最糟糕的是加密数据,它在设计上缺乏规律性)。它适用于大多数文本文件;在下一个示例中,我们使用 算法压缩和解压缩由从一个小句子中随机选择的 1,000 个单词组成的文本流。这还演示了链接后备存储流、装饰器流和适配器( 中本章开头所述)以及异步方法的使用:

string[] words = "The quick brown fox jumps over the lazy dog".Split();Random rand = new Random (0);   // Give it a seed for consistencyusing (Stream s = File.Create ("compressed.bin"))using (Stream ds = new BrotliStream (s, CompressionMode.Compress))using (TextWriter w = new StreamWriter (ds))  for (int i = 0; i < 1000; i++)    await w.WriteAsync (words [rand.Next (words.Length)] + " ");Console.WriteLine (new FileInfo ("compressed.bin").Length);      // 808using (Stream s = File.OpenRead ("compressed.bin"))using (Stream ds = new BrotliStream (s, CompressionMode.Decompress))using (TextReader r = new StreamReader (ds))  Console.Write (await r.ReadToEndAsync());  // Output below:lazy lazy the fox the quick The brown fox jumps over fox over fox Thebrown brown brown over brown quick fox brown dog dog lazy fox dog brownover fox jumps lazy lazy quick The jumps fox jumps The over jumps dog...

在这种情况下,BrotliStream 可以有效地压缩到 808 字节,每个字不到一个字节。(为了进行比较,DeflateStream 将相同的数据压缩到 885 字节。

在内存中压缩

有时,您需要完全压缩内存。以下是使用MemoryStream来实现此目的的方法:

byte[] data = new byte[1000];          // We can expect a good compression                                       // ratio from an empty array!var ms = new MemoryStream();using (Stream ds = new DeflateStream (ms, CompressionMode.Compress))  ds.Write (data, 0, data.Length);byte[] compressed = ms.ToArray();Console.WriteLine (compressed.Length);       // 11// Decompress back to the data array:ms = new MemoryStream (compressed);using (Stream ds = new DeflateStream (ms, CompressionMode.Decompress))  for (int i = 0; i < 1000; i += ds.Read (data, i, 1000 - i));

围绕 DeflateStream 的 using 语句以教科书的方式关闭它,刷新过程中任何未写入的缓冲区。这也会关闭它包装的 MemoryStream,这意味着我们必须调用 ToArray 来提取它的数据。

下面是避免关闭 MemoryStream 并使用异步读取和写入方法的替代方法:

byte[] data = new byte[1000];MemoryStream ms = new MemoryStream();using (Stream ds = new DeflateStream (ms, CompressionMode.Compress, true))  await ds.WriteAsync (data, 0, data.Length);Console.WriteLine (ms.Length);             // 113ms.Position = 0;using (Stream ds = new DeflateStream (ms, CompressionMode.Decompress))  for (int i = 0; i < 1000; i += await ds.ReadAsync (data, i, 1000 - i));

发送到 DeflateStream 的构造函数的附加标志指示它不要遵循通常的协议,即处理底层流。换句话说,MemoryStream保持打开状态,允许我们将其定位回零并重新读取。

Unix gzip 文件压缩

GZipStream的压缩算法在Unix系统上作为一种文件压缩格式很流行。每个源文件都压缩到具有扩展名的单独目标文件中。

以下方法执行 Unix 命令行 gzip 和 gunzip 实用程序的工作:

async Task GZip (string sourcefile, bool deleteSource = true){  var gzipfile = $"{sourcefile}.gz";  if (File.Exists (gzipfile))    throw new Exception ("Gzip file already exists");  // Compress  using (FileStream inStream = File.Open (sourcefile, FileMode.Open))  using (FileStream outStream = new FileStream (gzipfile, FileMode.CreateNew))  using (GZipStream gzipStream =     new GZipStream (outStream, CompressionMode.Compress))    await inStream.CopyToAsync (gzipStream);     if (deleteSource) File.Delete(sourcefile);}async Task GUnzip (string gzipfile, bool deleteGzip = true){  if (Path.GetExtension (gzipfile) != ".gz")     throw new Exception ("Not a gzip file");  var uncompressedFile = gzipfile.Substring (0, gzipfile.Length - 3);  if (File.Exists (uncompressedFile))     throw new Exception ("Destination file already exists");  // Uncompress  using (FileStream uncompressToStream =          File.Open (uncompressedFile, FileMode.Create))  using (FileStream zipfileStream = File.Open (gzipfile, FileMode.Open))  using (var unzipStream =          new GZipStream (zipfileStream, CompressionMode.Decompress))    await unzipStream.CopyToAsync (uncompressToStream);      if (deleteGzip) File.Delete (gzipfile);}

下面压缩文件:

await GZip ("/tmp/myfile.txt");      // Creates /tmp/myfile.txt.gz

下面解压缩它:

await GUnzip ("/tmp/myfile.txt.gz")  // Creates /tmp/myfile.txt
使用 ZIP 文件

System.IO.Compression 中的 ZipArchive 和 ZipFile 类支持 ZIP 压缩格式。ZIP格式相对于DeflateStream和GZipStream的优势在于它充当多个文件的容器,并与使用Windows资源管理器创建的ZIP文件兼容。

注意

ZipArchive 和 ZipFile 在 Windows 和 Unix 中都可以工作;但是,该格式在 Windows 中最流行。在Unix中,格式作为多个文件的容器更受欢迎。您可以使用第三方库(如 SharpZipLib)读取/写入文件。

ZipArchive 处理流,而 ZipFile 解决处理文件的更常见情况。(ZipFile 是 ZipArchive 的静态帮助程序类。

ZipFile 的 CreateFromDirectory 方法将指定目录中的所有文件添加到一个 ZIP 文件中:

ZipFile.CreateFromDirectory (@"d:\MyFolder", @"d:\archive.zip");

ExtractToDirectory 执行相反的操作,将 ZIP 文件解压缩到目录中:

ZipFile.ExtractToDirectory (@"d:\archive.zip", @"d:\MyFolder");

压缩时,您可以指定是否优化文件大小或速度,以及是否在存档中包含源目录的名称。在我们的示例中启用后一个选项将在存档中创建一个名为 的子目录,压缩文件将进入该子目录。

ZipFile 有一个用于读取/写入单个条目的 Open 方法。这将返回一个 ZipArchive 对象(您也可以通过使用 Stream 对象实例化 ZipArchive 来获取该对象)。调用 Open 时,必须指定文件名并指示是要读取、创建还是更新存档。然后,您可以通过 Entries 属性枚举现有条目,或者通过调用 来查找特定文件:

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 方法(这实际上是 ZipFileExtensions 类中的一个扩展方法)和一个返回可读/可写 Stream 的 Open 方法。您可以通过在 ZipArchive 上调用 CreateEntry(或 CreateEntryFromFile 扩展方法)来创建新条目。下面在归档文件中名为 的目录结构下创建归档 ,并向其添加 :

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);

您可以通过使用 MemoryStream 构建 ZipArchive 来完全在内存中执行相同的操作。

文件和目录操作

System.IO 命名空间提供了一组用于执行“实用程序”文件和目录操作的类型,例如复制和移动、创建目录以及设置文件属性和权限。对于大多数功能,您可以在两个类之间进行选择,一个提供静态方法,另一个提供实例方法:

静态类

文件和目录

实例方法类(使用文件或目录名称构造)

文件信息和目录信息

此外,还有一个名为 路径 的静态类 .这对文件或目录没有任何作用;相反,它为文件名和目录路径提供了字符串操作方法。路径还有助于处理临时文件。

文件类

File 是一个静态类,其方法都接受文件名。文件名可以是相对于当前目录的文件名,也可以是目录的完全限定名。以下是它的方法(所有公共和静态):

bool Exists (string path);      // Returns true if the file is presentvoid 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);      // UTC versions areDateTime GetLastAccessTime (string path);      // also provided.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 将引发异常;替换不。这两种方法都允许重命名文件以及移动到另一个目录。

如果文件标记为只读,则删除会引发未经授权的访问异常;你可以通过调用 GetAttributes 来提前判断这一点。如果操作系统拒绝对进程执行该文件的删除权限,它还会引发该异常。以下是 GetAttributes 返回的 FileAttribute 枚举的所有成员:

Archive, Compressed, Device, Directory, Encrypted,Hidden, IntegritySystem, Normal, NoScrubData, NotContentIndexed, Offline, ReadOnly, ReparsePoint, SparseFile, System, Temporary

此枚举中的成员是可组合的。下面介绍了如何切换单个文件属性而不打乱其余属性:

string filePath = "test.txt";FileAttributes fa = File.GetAttributes (filePath);if ((fa & FileAttributes.ReadOnly) != 0){    // Use the exclusive-or operator (^) to toggle the ReadOnly flag    fa ^= FileAttributes.ReadOnly;    File.SetAttributes (filePath, fa);}// Now we can delete the file, for instance:File.Delete (filePath);
注意

FileInfo 提供了一种更简单的方法来更改文件的只读标志:

new FileInfo ("test.txt").IsReadOnly = false;
压缩和加密属性注意

此功能仅适用于 Windows,需要 NuGet 包 System.Management 。

“压缩”和“加密”文件属性对应于 Windows 资源管理器中文件或目录的“属性”对话框中的压缩和加密复选框。这种类型的压缩和加密是,因为操作系统在后台完成所有工作,允许您读取和写入纯数据。

不能使用 SetAttributes 更改文件的压缩或加密属性 - 如果尝试,它将以静默方式失败!在后一种情况下,解决方法很简单:改为在类中调用 Encrypt() 和 Decrypt() 方法。使用压缩,它更复杂;一种解决方案是在System.Management中使用Windows管理规范(WMI)API。以下方法压缩目录,如果成功,则返回 0(如果成功,则返回 WMI 错误代码):File

static uint CompressFolder (string folder, bool recursive){  string path = "Win32_Directory.Name='" + folder + "'";  using (ManagementObject dir = new ManagementObject (path))  using (ManagementBaseObject p = dir.GetMethodParameters ("CompressEx"))  {    p ["Recursive"] = recursive;    using (ManagementBaseObject result = dir.InvokeMethod ("CompressEx",                                                             p, null))      return (uint) result.Properties ["ReturnValue"].Value;  }}

要解压缩,请将 CompressEx 替换为 解压缩Ex 。

透明加密依赖于从登录用户的密码中设定的密钥。系统对经过身份验证的用户执行的密码更改具有鲁棒性,但如果通过管理员重置密码,则加密文件中的数据将无法恢复。

注意

透明加密和压缩需要特殊的文件系统支持。NTFS(最常在硬盘驱动器上使用)支持这些功能;CDFS(在 CD-ROM 上)和 FAT(在可移动介质卡上)则不然。

您可以确定卷是否支持使用 Win32 互操作进行压缩和加密:

using System;using System.IO;using System.Text;using System.ComponentModel;using System.Runtime.InteropServices;class SupportsCompressionEncryption{  const int SupportsCompression = 0x10;  const int SupportsEncryption = 0x20000;  [DllImport ("Kernel32.dll", SetLastError = true)]  extern static bool GetVolumeInformation (string vol, StringBuilder name,    int nameSize, out uint serialNum, out uint maxNameLen, out uint flags,    StringBuilder fileSysName, int fileSysNameSize);  static void Main()  {    uint serialNum, maxNameLen, flags;    bool ok = GetVolumeInformation (@"C:\", null, 0, out serialNum,                                    out maxNameLen, out flags, null, 0);    if (!ok)      throw new Win32Exception();    bool canCompress = (flags & SupportsCompression) != 0;    bool canEncrypt = (flags & SupportsEncryption) != 0;  }}
文件安全注意

此功能仅适用于 Windows,需要 NuGet 包 System.IO.FileSystem.AccessControl。

FileSecurity 类允许您查询和更改分配给用户和角色的操作系统权限(命名空间 System.Security.AccessControl)。

在此示例中,我们列出文件的现有权限,然后将写入权限分配给“用户”组:

using System;using System.IO;using System.Security.AccessControl;using System.Security.Principal;void ShowSecurity (FileSecurity sec){  AuthorizationRuleCollection rules = sec.GetAccessRules (true, true,                                                       typeof (NTAccount));  foreach (FileSystemAccessRule r in rules.Cast<FileSystemAccessRule>()    .OrderBy (rule => rule.IdentityReference.Value))  {    // e.g., MyDomain/Joe    Console.WriteLine ($"  {r.IdentityReference.Value}");    // Allow or Deny: e.g., FullControl    Console.WriteLine ($"    {r.FileSystemRights}: {r.AccessControlType}");  }}var file = "sectest.txt";File.WriteAllText (file, "File security test.");var sid = new SecurityIdentifier (WellKnownSidType.BuiltinUsersSid, null);string usersAccount = sid.Translate (typeof (NTAccount)).ToString();Console.WriteLine ($"User: {usersAccount}");FileSecurity sec = new FileSecurity (file,                          AccessControlSections.Owner |                          AccessControlSections.Group |                          AccessControlSections.Access);Console.WriteLine ("AFTER CREATE:");ShowSecurity(sec); // BUILTIN\Users doesn't have Write permissionsec.ModifyAccessRule (AccessControlModification.Add,    new FileSystemAccessRule (usersAccount, FileSystemRights.Write,                               AccessControlType.Allow),    out bool modified);Console.WriteLine ("AFTER MODIFY:");ShowSecurity (sec); // BUILTIN\Users has Write permission

稍后,我们在中再举一个例子。

目录类

静态 Directory 类提供了一组类似于 File 类中的方法 — 用于检查目录是否存在 ( 存在 ), 移动目录 ( 移动 ), 删除目录 ( 删除 ), 获取/设置创建或上次访问时间,以及获取/设置安全权限。此外,目录公开了以下静态方法:

string GetCurrentDirectory ();void   SetCurrentDirectory (string path);DirectoryInfo CreateDirectory  (string path);DirectoryInfo GetParent        (string path);string        GetDirectoryRoot (string path);string[] GetLogicalDrives(); // Gets mount points on Unix// The following methods all return full paths: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);
注意

最后三种方法可能比 Get* 变体更有效,因为它们是延迟计算的 - 在枚举序列时从文件系统获取数据。它们特别适合 LINQ 查询。

枚举* 和 Get* 方法被重载,以便也接受搜索模式(字符串)和搜索选项(枚举)参数。如果指定“搜索选项”。搜索所有子目录 ,执行递归子目录搜索。方法将 *Files 与 *Directory 的结果组合在一起。

如果目录尚不存在,请按以下步骤创建目录:

if (!Directory.Exists (@"d:\test"))  Directory.CreateDirectory (@"d:\test");
文件信息和目录信息

文件和目录上的静态方法便于执行单个文件或目录操作。如果需要在一行中调用一系列方法,FileInfo 和 DirectoryInfo 类将提供一个对象模型,使作业更容易。

FileInfo 以实例形式提供了大多数 File 的静态方法,以及一些附加属性,如扩展名、长度、IsReadOnly 和 Directory,用于返回 DirectoryInfo 对象。例如:

static string TestDirectory =>   RuntimeInformation.IsOSPlatform (OSPlatform.Windows)    ? @"C:\Temp"     : "/tmp"; Directory.CreateDirectory (TestDirectory);FileInfo fi = new FileInfo (Path.Combine (TestDirectory, "FileInfo.txt"));Console.WriteLine (fi.Exists);         // falseusing (TextWriter w = fi.CreateText())  w.Write ("Some text");Console.WriteLine (fi.Exists);         // false (still)fi.Refresh();Console.WriteLine (fi.Exists);         // trueConsole.WriteLine (fi.Name);           // FileInfo.txtConsole.WriteLine (fi.FullName);       // c:\temp\FileInfo.txt (Windows)                                       // /tmp/FileInfo.txt (Unix)Console.WriteLine (fi.DirectoryName);  // c:\temp (Windows)                                       // /tmp (Unix)Console.WriteLine (fi.Directory.Name); // tempConsole.WriteLine (fi.Extension);      // .txtConsole.WriteLine (fi.Length);         // 9fi.Encrypt();fi.Attributes ^= FileAttributes.Hidden;   // (Toggle hidden flag)fi.IsReadOnly = true;Console.WriteLine (fi.Attributes);    // ReadOnly,Archive,Hidden,EncryptedConsole.WriteLine (fi.CreationTime);  // 3/09/2019 1:24:05 PMfi.MoveTo (Path.Combine (TestDirectory, "FileInfoX.txt")); DirectoryInfo di = fi.Directory;Console.WriteLine (di.Name);             // temp or tmpConsole.WriteLine (di.FullName);         // c:\temp or /tmpConsole.WriteLine (di.Parent.FullName);  // c:\ or /di.CreateSubdirectory ("SubFolder");

下面介绍如何使用 DirectoryInfo 枚举文件和子目录:

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 类定义用于处理路径和的方法和字段。

假设此设置代码

string dir  = @"c:\mydir";    // or /mydirstring file = "myfile.txt";string path = @"c:\mydir\myfile.txt";    // or /mydir/myfile.txtDirectory.SetCurrentDirectory (@"k:\demo");    // or /demo

我们可以使用以下表达式演示 Path 的方法和字段:

表达

结果(Windows,然后是Unix)

Directory.GetCurrentDirectory()

k:\demo\ or /demo

Path.IsPathRoot (file)

Path.IsPathRoot (path)

Path.GetPathRoot (path)

c:\或/

Path.GetDirectoryName (path)

c:\mydir 或 /mydir

Path.GetFileName (path)

我的文件.txt

Path.GetFullPath (file)

k:\demo\myfile.txt 或

Path.Combined (dir, file)

c:\mydir\myfile.txt or /

文件扩展名:

Path.HasExtension (file)

Path.GetExtension (file)

。.txt

Path.GetFileNameWithoutExtension (file)

我的文件

Path.ChangeExtension (file, “.log”)

我的文件.log

分隔符和字符:

Path.DirectorySeparatorChar

\或/

Path.AltDirectorySeparatorChar

/

路径.路径分隔符

;或:

路径.卷分隔符字符

:或/

Path.GetInvalidPathChars()

字符 0 到 31 和“<>|或 0

Path.GetInvalid文件名字符()

字符 0 到 31 和“<>|:*?\/ 或 0 和 /

临时文件:

Path.GetTempPath()

\ 临时或

Path.GetRandomFileName()

d2dwuzjf.dnp

Path.GetTempFileName()

\ Temp \ 或 /.tmptmp14B.tmp

合并特别有用:它允许您组合一个目录和文件名(或两个目录),而无需首先检查是否存在尾随路径分隔符,并且它会自动为操作系统使用正确的路径分隔符。它提供最多接受四个目录和/或文件名的重载。

GetFullPath 将相对于当前目录的路径转换为绝对路径。它接受诸如 .。

GetRandomFileName 返回一个真正唯一的 8.3 个字符的文件名,而不实际创建任何文件。GetTempFileName 使用每 65,000 个文件重复一次的自动递增计数器生成一个临时文件名。然后,它在本地临时目录中创建此名称的零字节文件。

警告

完成后,必须删除由 GetTemp文件名生成的文件;否则,它最终将引发异常(在您第 65,000 次调用 GetTempFileName 之后)。如果这是一个问题,您可以改为将 GetTempPath 与 GetRandomFile Name 组合在一起。只是要小心不要填满用户的硬盘驱动器!

特殊文件夹

路径和目录中缺少的一件事是查找文件夹(”、“”、“”等)的方法。这由 System.Environment 类中的 GetFolderPath 方法提供:

string myDocPath = Environment.GetFolderPath  (Environment.SpecialFolder.MyDocuments);

Environment.SpecialFolder是一个枚举,其值包含Windows中的所有特殊目录,例如AdminTools,ApplicationData,Fonts,History,SendTo,StartMenu等。此处介绍了除 .NET 运行时目录之外的所有内容,您可以按如下方式获取该目录:

System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory()
注意

大多数特殊文件夹在 Unix 没有分配路径。以下路径在 Ubuntu Linux 18.04 桌面上:ApplicationData 、CommonApplicationData 、Desktop 、DesktopDirectory 、LocalApplicationData 、MyDocuments 、MyMusic 、MyPictures 、MyVideos、Templates 和 。

在Windows系统上特别有价值的是应用程序数据 :在这里,您可以存储与用户一起通过网络传输的设置(如果在网络域上启用了漫游配置文件), LocalApplicationData ,用于非漫游数据(特定于登录用户)和 CommonApplicationData ,由计算机的每个用户共享。将应用程序数据写入这些文件夹被认为比使用 Windows 注册表更可取。在这些文件夹中存储数据的标准协议是使用应用程序的名称创建一个子目录:

string localAppDataPath = Path.Combine (  Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData),  "MyCoolApplication");if (!Directory.Exists (localAppDataPath))  Directory.CreateDirectory (localAppDataPath);

使用CommonApplicationData时有一个可怕的陷阱:如果用户使用管理提升启动程序,然后程序在CommonApplicationData中创建文件夹和文件,则该用户可能缺少以后在受限Windows登录下运行时替换这些文件的权限。(在受限权限帐户之间切换时也存在类似的问题。您可以通过创建所需的文件夹(将权限分配给每个人)作为设置的一部分来解决此问题。

将配置和日志文件写入的另一个位置是将配置和日志文件写入应用程序的基目录,您可以使用 AppDomain.CurrentDomain.BaseDirectory 获取该目录。但是,不建议这样做,因为操作系统可能会拒绝应用程序在初始安装后写入此文件夹的权限(无管理)。

查询卷信息

您可以使用 DriveInfo 类查询计算机上的驱动器:

DriveInfo c = new DriveInfo ("C");       // Query the C: drive.                                         // On Unix: /long totalSize = c.TotalSize;            // Size in bytes.long freeBytes = c.TotalFreeSpace;       // Ignores disk quotas.long freeToMe  = c.AvailableFreeSpace;   // Takes quotas into account.foreach (DriveInfo d in DriveInfo.GetDrives())  // All defined drives.                                                // On Unix: mount points{  Console.WriteLine (d.Name);             // C:\  Console.WriteLine (d.DriveType);        // Fixed  Console.WriteLine (d.RootDirectory);    // C:\  if (d.IsReady)   // If the drive is not ready, the following two                   // properties will throw exceptions:  {    Console.WriteLine (d.VolumeLabel);    // The Sea Drive    Console.WriteLine (d.DriveFormat);    // NTFS  }}

静态 GetDrives 方法返回所有映射的驱动器,包括 CD-ROM、媒体卡和网络连接。驱动器类型是具有以下值的枚举:

Unknown, NoRootDirectory, Removable, Fixed, Network, CDRom, Ram
捕获文件系统事件

类允许您监视目录(以及可选的子目录)的活动。文件系统观察器具有在创建、修改、重命名和删除文件或子目录时以及当其属性更改时触发的事件。无论执行更改的用户或进程如何,都会触发这些事件。下面是一个示例:

Watch (GetTestDirectory(), "*.txt", true);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();  }  // Disposing the FileSystemWatcher stops further events from firing.}void FileCreatedChangedDeleted (object o, FileSystemEventArgs e)  => Console.WriteLine ("File {0} has been {1}", e.FullPath, e.ChangeType);void FileRenamed (object o, RenamedEventArgs e)  => Console.WriteLine ("Renamed: {0}->{1}", e.OldFullPath, e.FullPath);void FileError (object o, ErrorEventArgs e)  => Console.WriteLine ("Error: " + e.GetException().Message);string GetTestDirectory() =>  RuntimeInformation.IsOSPlatform (OSPlatform.Windows)    ? @"C:\Temp"    : "/tmp"; 
注意

由于 FileSystemWatcher 在单独的线程上引发事件,因此必须对事件处理代码进行异常处理,以防止错误导致应用程序关闭。更多信息请参阅

错误事件不会通知您文件系统错误;相反,它表示文件系统观察器的事件缓冲区溢出,因为它被更改、创建、删除或重命名的事件淹没。可以通过 InternalBufferSize 属性更改缓冲区大小。

包含子目录以递归方式应用。因此,如果在 上创建一个 FileSystemWatcher,并且 IncludeSubdirectory true ,当文件或目录在硬盘驱动器上的任何位置发生更改时,其事件将触发。

警告

使用 FileSystemWatcher 的一个陷阱是在文件完全填充或更新之前打开并读取新创建或更新的文件。如果您正在与创建文件的其他一些软件配合使用,则可能需要考虑一些策略来缓解此问题,例如创建具有未监视扩展名的文件,然后在完全写入后重命名它们。

操作系统安全性

所有应用程序都受操作系统限制的约束,具体取决于用户的登录权限。这些限制会影响文件 I/O 以及其他功能,例如对 Windows 注册表的访问。

在Windows和Unix中,有两种类型的帐户:

对访问本地计算机不施加任何限制的管理/超级用户帐户限制管理功能和其他用户数据的可见性的受限权限帐户

在Windows上,称为用户帐户控制(UAC)的功能意味着管理员在登录时会收到两个令牌或“帽子”:管理帽子和普通用户帽子。默认情况下,程序戴着普通用户帽(权限受限)运行,除非程序请求。然后,用户必须在显示的对话框中批准请求。

在Unix上,用户通常使用受限帐户登录。对于管理员来说,减少无意中损坏系统的可能性也是如此。当用户需要运行需要提升权限的命令时,他们在命令前面加上 sudo(超级用户 do 的缩写)。

,应用程序将使用受限的用户权限运行。这意味着您必须执行以下操作之一:

编写应用程序,使其无需管理权限即可运行。在应用程序清单中请求管理提升(仅限 Windows),或检测缺少所需权限并提醒用户以管理员/超级用户身份重新启动应用程序。

第一个选项对用户来说更安全、更方便。在大多数情况下,将程序设计为在没有管理权限的情况下运行很容易。

您可以按方式了解您是否在管理帐户下运行:

[DllImport("libc")]public static extern uint getuid();static bool IsRunningAsAdmin(){  if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows))  {    using var identity = WindowsIdentity.GetCurrent();    var principal = new WindowsPrincipal (identity);    return principal.IsInRole (WindowsBuiltInRole.Administrator);  }  return getuid() == 0;}

在 Windows 上启用 UAC 后,仅当当前进程具有管理提升时,才会返回 true。在 Linux 上,仅当当前进程以超级用户身份运行时,它才返回 true(例如,)。

在标准用户帐户中运行

以下是标准用户帐户中执行的关键操作:

写入以下目录:操作系统文件夹(通常为 或 /)和子目录程序文件文件夹( 或 /)和子目录操作系统驱动器的根目录(例如, 或 /)写入注册表的HKEY_LOCAL_MACHINE分支 (Windows)读取性能监视 (WMI) 数据 (Windows)

此外,作为普通 Windows 用户(甚至管理员),您可能会被拒绝访问属于其他用户的文件或资源。Windows 使用访问控制列表 (ACL) 系统来保护此类资源 — 您可以通过 System.Security.AccessControl 中的类型查询和断言您在 ACL 中的权限。ACL 也可以应用于跨进程等待句柄,如所述。

如果由于操作系统安全性而拒绝访问任何内容,CLR 将检测到故障并引发 UnauthorizedAccessException(而不是以静默方式失败)。

在大多数情况下,您可以按如下方式处理标准用户限制:

将文件写入其建议的位置。避免将注册表用于可存储在文件中的信息(HKEY_CURRENT_USER配置单元除外,你只能在 Windows 上对其具有读/写访问权限)。在安装过程中注册 ActiveX 或 COM 组件(仅限 Windows)。

用户文档的推荐位置是 SpecialFolder.MyDocuments :

string docsFolder = Environment.GetFolderPath                    (Environment.SpecialFolder.MyDocuments);string path = Path.Combine (docsFolder, "test.txt");

用户可能需要在应用程序外部修改的配置文件的建议位置是 SpecialFolder.ApplicationData(仅限当前用户)或 SpecialFolder.CommonApplicationData(所有用户)。您通常根据组织和产品名称在这些文件夹中创建子目录。

管理提升和虚拟化

使用,可以请求 Windows 在运行程序时提示用户进行管理提升(Linux 忽略此请求):

<?xml version="1.0" encoding="utf-8"?><assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">    <security>      <requestedPrivileges>        <requestedExecutionLevel level="requireAdministrator" />      </requestedPrivileges>    </security>  </trustInfo></assembly>

(我们将在第 中更详细地描述应用程序清单。

如果将 requireAdministrator 替换为 asInvoker ,它会指示 Windows 不需要管理提升。其效果几乎与根本没有应用程序清单相同,只是。虚拟化是随 Windows Vista 引入的一项临时措施,可帮助旧应用程序在没有管理权限的情况下正确运行。缺少具有 requestExecutionLevel 元素的应用程序清单将激活此向后兼容性功能。

当应用程序写入或 目录或注册表的HKEY_LOCAL_MACHINE区域时,虚拟化将发挥作用。更改不会引发异常,而是重定向到硬盘上不会影响原始数据的单独位置。这可以防止应用程序干扰操作系统或其他行为良好的应用程序。

内存映射文件

提供两个关键功能:

高效随机访问文件数据在同一台计算机上的不同进程之间共享内存的能力

内存映射文件的类型驻留在 System.IO.MemoryMappedFiles 命名空间中。在内部,它们通过包装操作系统的 API 来存储内存映射文件。

内存映射文件和随机文件 I/O

尽管普通文件流允许随机文件 I/O(通过设置流的 Position 属性),但它针对顺序 I/O 进行了优化。作为粗略的经验法则:

对于顺序 I/O,FileStream 大约比内存映射文件快 10 倍。对于随机 I/O,内存映射文件大约比 FileStream 快 10 倍。

更改文件流的位置可能会花费几微秒 - 如果在循环中完成,则会加起来。FileStream 也不适合多线程访问,因为它的位置会随着读取或写入而变化。

要创建内存映射文件:

像往常一样获取文件流。实例化一个 MemoryMappedFile ,传入文件流。在内存映射文件对象上调用 CreateViewAccessor。

最后一步为您提供了一个 MemoryMappedViewAccessor 对象,该对象提供了用于随机读取和写入简单类型、结构和数组的方法(在中对此进行了详细介绍)。

The following creates a one million-byte file and then uses the memory-mapped file API to read and then write a byte at position 500,000:

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

您还可以在调用 创建从文件 时指定映射名称和容量。指定非空映射名称允许内存块与其他进程共享(请参阅下一节);指定容量会自动将文件放大到该值。以下内容将创建一个 1,000 字节的文件:

File.WriteAllBytes ("short.bin", new byte [1]);using (var mmf = MemoryMappedFile.CreateFromFile                 ("short.bin", FileMode.Create, null, 1000))  ...
内存映射文件和共享内存 (Windows)

在 Windows 下,还可以使用内存映射文件作为在同一台计算机上的进程之间共享内存的方法。一个进程通过调用 MemoryMappedFile.CreateNew 来创建共享内存块,然后其他进程通过调用具有相同名称的 MemoryMappedFile.Open存在来订阅相同的内存块。尽管它仍称为内存映射“文件”,但它完全驻留在内存中,不存在磁盘。

以下代码创建一个 500 字节的共享内存映射文件,并将整数 12345 写入位置 0:

using (MemoryMappedFile mmFile = MemoryMappedFile.CreateNew ("Demo", 500))using (MemoryMappedViewAccessor accessor = mmFile.CreateViewAccessor()){  accessor.Write (0, 12345);  Console.ReadLine();   // Keep shared memory alive until user hits Enter.}

以下代码打开该内存映射文件并读取该整数:

// This can run in a separate executable:using (MemoryMappedFile mmFile = MemoryMappedFile.OpenExisting ("Demo"))using (MemoryMappedViewAccessor accessor = mmFile.CreateViewAccessor())  Console.WriteLine (accessor.ReadInt32 (0));   // 12345
跨平台进程间共享内存

Windows和Unix都允许多个进程对同一个文件进行内存映射。您必须小心确保适当的文件共享设置:

static void Writer(){  var file = Path.Combine (TestDirectory, "interprocess.bin");  File.WriteAllBytes (file, new byte [100]);  using FileStream fs =     new FileStream (file, FileMode.Open, FileAccess.ReadWrite,                     FileShare.ReadWrite);                      using MemoryMappedFile mmf = MemoryMappedFile    .CreateFromFile (fs, null, fs.Length, MemoryMappedFileAccess.ReadWrite,                     HandleInheritability.None, true);  using MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor();      accessor.Write (0, 12345);  Console.ReadLine();   // Keep shared memory alive until user hits Enter.  File.Delete (file);}static void Reader(){  // This can run in a separate executable:  var file = Path.Combine (TestDirectory, "interprocess.bin");  using FileStream fs =     new FileStream (file, FileMode.Open, FileAccess.ReadWrite,                     FileShare.ReadWrite);  using MemoryMappedFile mmf = MemoryMappedFile    .CreateFromFile (fs, null, fs.Length, MemoryMappedFileAccess.ReadWrite,                       HandleInheritability.None, true);  using MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor();    Console.WriteLine (accessor.ReadInt32 (0));   // 12345}  static string TestDirectory =>  RuntimeInformation.IsOSPlatform (OSPlatform.Windows)    ?  @"C:\Test"    : "/tmp";
使用视图访问器

在 MemoryMappedFile 上调用 CreateViewAccessor 会给你一个视图访问器,让你在随机位置读/写值。

Read* / Write* 方法接受数值类型、布尔值和字符,以及包含值类型元素或字段的数组和结构。禁止引用类型(以及包含引用类型的数组或结构),因为它们无法映射到非托管内存。因此,如果要编写字符串,则必须将其编码为字节数组:

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

下面是读取/写入结构的示例:

struct Data { public int X, Y; }...var data = new Data { X = 123, Y = 456 };accessor.Write (0, ref data);accessor.Read (0, out data);Console.WriteLine (data.X + " " + data.Y);   // 123 456

读取和写入方法出奇地慢。通过指针直接访问基础非托管内存,可以获得更好的性能。继上一个示例之后:

unsafe{  byte* pointer = null;  try  {    accessor.SafeMemoryMappedViewHandle.AcquirePointer (ref pointer);    int* intPointer = (int*) pointer;    Console.WriteLine (*intPointer);               // 123  }  finally  {    if (pointer != null)      accessor.SafeMemoryMappedViewHandle.ReleasePointer();  }}

必须将项目配置为允许不安全的代码。您可以通过编辑 .csproj 文件来执行此操作:

  <PropertyGroup>    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>  </PropertyGroup>

指针的性能优势在处理大型结构时更加明显,因为它们允许您直接处理原始数据,而不是使用读/写在托管和非托管内存之间数据。我们将在第章中进一步探讨这一点。

标签: #ubuntuacl设置删除权限 #简述链接器命令文件中的memory和sections