Chapter 14. Streams and I/O

本章探讨在.NET中用于输入输出的基础类型,着重强调以下主题:

  • .NET流体系结构以及他如何为跨越种种IO类型的读取提供了一致的编程接口
  • 操作磁盘上的文件与目录
  • 独立存储及其在隔离程序与用户数据中的角色

本章关注System.IO包中的类型,底层IO功能的大本营。.NET框架同时以各种形式提供了高层的IO功能,例如SQL连接与命令,LINQ到SQL以及LINQ到XML,WCF,Web Services以及远程等。

流体系结构

.NET流体系结构由三个概念组成:后端存储,装饰器以及适配器,如图14-1所示:

后端存储(backing store)是使得输入输出有用的端点,例如文件或是网络连接。更确切的说,他是下列中的一个或同时是两个:

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

后端存储是没有用的,除非向程序员公开。Stream是用该目的的标准.NET类;他公开了一个用于读取,写入与定位的标准方法集合。与数组不同,其后端存储只在内存中存在一次,流序化处理数据-一次一个字节或是可管理尺寸的块。所以,流可以使用较小的内存,而不论其后端存储的尺寸。

csharp_14_1.png

csharp_14_1.png

流分为两类:

  • 后端存储流:这是与某种后端存储类型直接相关的,例如FileStream或NetworkStream
  • 装饰流:这是由另一个流中获取的流,以某种方式转换数据,例如DeflateStream或CryptoStream。

装饰流具有下列的体系结构优点:

  • 这将后端流由需要自己实现例如压缩或是加密这样的特性中解放出来
  • 当被装饰后,流不再忍受接口变化的痛苦
  • 我们在运行时连接到装饰器
  • 我们可以将装饰器链接在一起

后端存储与装饰器流只以字节方式进行处理。尽管这比较灵活与高效,但是程序通常在更高的层次上进行处理,例如文本或是XML。适配器通过将流封装一个具有特殊方法来处理特定格式的类中来弥补这一缺陷。例如,一个文本读取器公开了一个ReadLine方法;一个XML写入器公开了WriteAttributes方法。

总结起来就是,后端存储流提供了原始数据;装饰器流提供了透明的二进制转换,例如加密;适配器提供了类型方法来以高级类型形式处理数据,例如字符串与XML。图14-1显示他们之间的关系。要形成一个链,我们只需要简单的将一个对象传递给另一个的构造器。

使用流

抽象的Stream类是所有流的基类。他为三个基础操作定义了方法与属性:读取,写入与定位,以及管理任务例如关闭,输出缓冲以及配置超时(如表14-1所示)。

csharp_table_14_1.png

csharp_table_14_1.png

在下面的示例中,我们使用一个文件流来读取,写入并定位:

using System;
using System.IO;
class Program
{
  static void Main()
  {
    // 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
    }
  }
}

读取与写入

一个流也许支持读取,写入或是两者。如果CanWrite返回false,则流是只读的;如果CanRead返回false,则流是只写的。

Read由流中读取一个数据块并存入一个数组。他返回所读取的字节数,返回值总是小于或是等于count参数。如果返回值小于count,则意味着或者是到达了流的结尾,或者是流以更小的块向我们提供数据(在网络流中经常如此)。在任何一种情况下,数组中余下的字节都会保持不可写,从而保存他们以前的值。

下面是读取1000个字节流的正确方式:

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

ReadBye方法更为简单:他仅读取一个字节,返回-1表示到达了流的结尾。ReadByte实际上返回int而不是byte,因为后者不能返回-1。

Write与WriteByte方法向流输出数据。如果他们不能输出指定的字节数,则会抛出异常。

注意,在Read与Write方法中,offset参数指读取或写在buffer数组中开始的索引,而不是流中的位置。

流也可以通过BeginRead与BeginWrite方法支持异步读取与写入。异步方法本意是用于高吞吐量的服务器程序,我们会在第23章中进行描述。

定位

如果CanSeek方法返回true,则流是可定位的。对于可定位的流(例如文件流),我们可以查询或是修改其Length(通过调用SetLenght),并且在任何时刻修改我们正在读取或是写入的Position。Position属性是相对于流的起始处的;然而,Seek方法可以使得我们相对于当前位置或是文件的结束处进行移动。

注意,在FileStream上改变Position通常会花费几毫秒的时间。如果我们在一个循环中执行几百万次,框架4.0中新的MemoryMappedFile类是比FileStream更好的选择。

对于不可定位的流(例如加密流),确定其长度的唯一方法就是完全读取。而且,如果我们需要重新读取前面的一部分,我们必须关闭流并重新读取。

关闭与输出缓冲

流在使用之后必须销毁来释放底层资源,例如文件或是套接字句柄。保证关闭流的最简单方法是在using块中实例化流。通常,流遵循标准的销毁语义:

  • Dispose与Close在功能上是相同的
  • 重复销毁或是关闭流不会引起错误

关闭一个装饰器流会同时关闭装饰器以及其后端存储流。对于一个装饰器链,关闭最外层的装饰器(位于链的头部)会关闭整个装饰器链。

某些流在内部会缓冲要写入后端存储的数据或是由后端存储读取的数据来减少读取的来回次数从而改进性能(文件流就是一个好例子)。这意味着我们要写入流的数据也许并没有立即写入后端存储;写入会被延迟直到缓冲区被填满。Flush方法会强制在内部缓冲的数据被立即写入。Flush方法在流被关闭时自动调用的,所以我们不需要执行下面的语句:

s.Flush(); s.Close();

超时

如果CanTimeout返回true,则流支持读写超时。网络支持超时;文件流与内存流则不支持。对于支持超时的流,ReadTimeout与WriteTimeout属性决定了所需要的超时时间,以毫秒计,0则为不超时。Read与Write方法通过抛出异常来表明发生了超时。

线程安全

作为一条规则,流不是线程安全的,意味着两个线程不能同时读取或是写入相同的流,以避免错误。Stream类通过静态的Synchronized方法提供了一个简单的解决办法。这个方法接受一个任意类型的流并返回一个线程安全的包装器。包装器通过获取读取,写入或是定位上的一个排他锁来进行工作,从而保证在任意时刻只有一个线程可以执行这样的操作。实践中,这会使得多个线程同时向相同的流中添加数据-其他的活动(例如并行读取)需要额外的锁来保证每一个线程访问流的确定部分。我们将会在第21章中探讨线程安全。

后端存储流

图14-2显示了.NET框架所提供的关键后端存储流。同时还有一个“空流”,是通过Stream的静态Null域来提供的。

在下面的部分中,我们描述FileStream与MemoryStream;在本章的最后一部分,我们描述IsolatedStorageStream。在第15章中,我们探讨NetworkStream。

csharp_14_2.png

csharp_14_2.png

FileStream

在本章前面的内容中,我们演示了使用FileStream来读取与写入数据字节的基本用法。接下来我们将会探讨这个类的特性。

构建FileStream

实例化一个FileStream的最简单方法就是使用下面File类的静态方法中的一个:

FileStream fs1 = File.OpenRead  ("readme.bin");            // Read-only
FileStream fs2 = File.OpenWrite (@"c:\temp\writeme.tmp");  // Write-only
FileStream fs3 = File.Create    (@"c:\temp\writeme.tmp");  // Read/write

如果文件已经存在,则OpenWrite与Create的行为会有所不同。Create会清除已存在内容;OpenWrite会完整的保留已存在的内容并将流定位在起始处。如果我们写入的字节小于文件中已存在的字节数,则OpenWrite会留给我们一个新旧内容的混合体。

我们也可以直接初始化一个FileStream。其构造器提供了到其每一个特性的访问,使得我们可以指定文件名或是底层文件句柄,文件创建与访问模式,以及共享,缓冲,安全等选项。下面的代码打开一个已存在的文件用于读写访问而不覆盖:

var fs = new FileStream (“readwrite.tmp”, FileMode.Open); // Read/write

稍后我们会探讨FileMode的更多内容。

指定文件名

文件名可以是绝对路径或是相对于当前目录的相对路径。我们可以通过静态的Environment.CurrentDirectory属性来修改当前目录。

AppDomain.CurrentDomain.BaseDirectory返回程序基目录,通常情况下是包含可执行程序的目录。要指定一个相对于该目录的文件名,我们可以调用Path.Combine方法:

string baseFolder = AppDomain.CurrentDomain.BaseDirectory;
string logoPath = Path.Combine (baseFolder, "logo.jpg");
Console.WriteLine (File.Exists (logoPath));

我们可以通过UNC路径来进行跨网络的读写,例如 \\JoesPC\PicShare\pic.jpg or \\10.1.1.2\PicShare\pic.jpg。

指定FileMode

所有接受文件名的FileStream构造器同时也需要一个FileMode枚举参数。图14-3显示了如何选择FileMode,以及选择所产生的类似于在File类上调用静态方法的结果。

csharp_14_3.png

csharp_14_3.png

仅使用文件名或是FileMode构造FileStream会为我们构建一个可读或写的流。如果我们同时提供FileAccess参数,我们就可以进一步请求相应的访问模式:

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

下面的代码会返回一个只读流,与调用File.OpenRead等同:

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

FileModel.Append是一个奇怪的访问模式,使用这个模式我们可以获得一个只写的流。要添加读写支持,我们必须使用FileMode.Open或是FileMode.OpenOrCreate,然后定位到流的结尾:

using (var fs = new FileStream ("myFile.bin", FileMode.Open))
{
  fs.Seek (0, SeekOrigin.End);
  ...

高级FileStream特性

下面是当我们构建FileStream时我们可以包含的其他参数:

  • FileShare枚举,描述了在我们完成之前向深入相同文件的其他进程授予多个权限(None,Read(默认),ReadWrite或是Write)。
  • 以字节表示的内部缓冲区的大小(当前默认值为4KB)
  • 表明是否推迟操作系统异步IO的标记
  • FileSecurity对象,描述了向新文件赋值哪种用户与角色权限
  • 请求操作系统加密(Encrypted)的FileOptions标记,他会在临时文件关闭时自动删除(DeleteOnClose)以及优化建议(RandomAccess与SequentialScan)。同时有一个请求操作系统禁止write-behind缓存的WriteThrough标记;这用于相互影响的文件或是日志。

使用FileShare.ReadWrite打开文件会使得其他进程或是用户同时读写相同的文件。为了避免混乱,我们可以使用下面的方法在读写之前锁定文件的特定部分:

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

如果所请求的文件部分已经被锁定了,Lock则抛出异常。这为系统用于基于文件的数据库,例如Access与FoxPro。

MemoryStream

MemoryStream使用数组作为后端存储。这在某种程度上破坏了拥有流的目的,因为整个的后端存储只在内存中存在一次。然而,MemoryStream依然有用;当我们需要随机访问一个不可定位的流时就是一个好例子。如果我们知道源流将是可管理尺寸的,那么我们就可以将其拷贝到MemoryStream中,如下所示:

static MemoryStream ToMemoryStream (this Stream input, bool closeInput)
{
  try
  {                                         // Read and write in
    byte[] block = new byte [0×1000];       // blocks of 4K.
    MemoryStream ms = new MemoryStream();
    while (true)
    {
      int bytesRead = input.Read (block, 0, block.Length);
      if (bytesRead == 0) return ms;
      ms.Write (block, 0, bytesRead);
    }
  }
  finally { if (closeInput) input.Close (); }
}

closeInput输入参数的目的就是为了避免方法作者以及消费者认为对方关闭流的情况。

我们可以通过调用ToArray将一个MemoryStream转换为一个字节数组。GetBuffer方法通过直接引用底层存储数组可以高效的完成相同的工作;不幸的是,这个数组通常要长于流的实际长度。

我们可以在本章稍后的部分中看到更多MemoryStream的例子。

PipeStream

PipeStream是在框架3.5中引入的。他提供了一个简单的方法,通过这个方法一个进程可以通过Windows管道协议与另一个进程进行交互。有两种管道类型:

  • 匿名管道:允许在相同的计算机上的父子进程之间的单向通信
  • 命名管道:允许在相同的计算机上或是跨Windows网络的不同计算机上的任意进程之间的双向通信

管道适于单个计算机上的进程间通信(IPC):他并不依赖于网络传输,从而可以获得良好的性能并且没有防火墙的问题。

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

  • 匿名管道:AnonymousPipeServerStream与AnonymousPipeClientStream
  • 命名管道:NamedPipeServerStream与NamedPipeClientStream

命名管道更易于使用,我们会首先进行描述。

命名管道

使用命名管道,参与者可以通过相同名字的管道进行通信。协议定义了两个不同的角色:客户端与服务器。客户端与服务器之间的通信如下:

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

然后两个参与者读取与写入流进行通信。

下面的示例演示了发送一个字节(100)并等待接收一个字节的服务器:

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);                 // Send the value 200 back.
}

默认情况下,命名管道流是双向的,所以任意一个参与者都可以读取或是写入流。这意味着客户端与服务器必须遵循相同的协议来协调他们的动作,从而两个参与者不会同时发送或是接收数据。

同时还需要在每次传输的长度上达到一致。我们的示例就是具有这样的考虑,因为在每个方向上我们限定为只有一个字节。为了处理多于一个字节的消息 ,管道提供了消息传输模式。如果允许了这种模式,调用Read的参与者可以通过检测IsMessageComplete属性来确定消息是否已经完成。为了进行演示,我们编写了一个助手方法,这个方法会由打开消息模式的PipeStream中读取完整的消息--换句话说,读取直到IsMessageComplete为真:

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

现在我们可以激活消息传输模式。在服务器端,这是通过在构建流时指定PipeTransmissionMode.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);
}

匿名管道

匿名管道在父子进程之间提供了单向通信。匿名管道并不使用系统的名字,而是通过私有句柄实现。

类似于命名管道,匿名管道也有客户端与服务器的角色。然而,通信系统略为不同,并且处理过程如下:

  1. 服务器端实例化AnonymousPipeServerStream,将PipeDirection设置为In或是Out。
  2. 服务器调用GetClientHandleAsString来获得一个管道的标记符,然后将其传递给客户端(通常在启动子进程时作为参数)。
  3. 子进程实例化AnonymousPipeClientStream,指定相反的PipeDirection。
  4. 服务器通过调用DisposeLocalCopyOfClientHandle来释放在第2步生成的局部句柄。
  5. 父进程与子进程通过读取/写入流来进行通信。

因为匿名管道是单向的,要实现双向通信,服务器必须创建两个管道。下面演示了一个向子进程发送一个字节并由子进程接受一个字节的服务器:

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;      // Required for child process
  Process p = Process.Start (startInfo);
  tx.DisposeLocalCopyOfClientHandle();    // Release unmanaged
  rx.DisposeLocalCopyOfClientHandle();    // handle resources.
  tx.WriteByte (100);
  Console.WriteLine ("Server received: " + rx.ReadByte());
  p.WaitForExit();
}

下面是编译为d:\PipeDemo\Cli-entDemo.exe的相对应的客户端代码:

string rxID = args[0];    // Note we're reversing the
string txID = args[1];    // receive and transmit roles.
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);
}

与命名管道类似,客户端与服务器必须协调他们的发送与接收以及每一个次传输的数据长度。不幸的是,匿名管道不支持消息模式,所以我们必须为消息长度约定实现我们自己的协议。一个解决方法就是在每一次传输之前发送四个字节,作为定义其后消息长度的整数值。BitConverter类提供了用于在整数与四字节数组之间进行转换的方法。

BufferedStream

BufferedStream装饰或是包装另一个流使其具有缓冲功能,而他是.NET框架中大量的装饰器流类型中的一个,如图14-4所示:

csharp_14_4.png

csharp_14_4.png

缓冲通过减少与后端存储的交互而改善性能。下面显示了我们如何将一个FileStream包装为一个20KB的BufferedStream:

// 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 buffer
{
  bs.ReadByte();
  Console.WriteLine (fs.Position);         // 20000
}

在这个示例中,由于读取缓冲,底层流在仅读取1个字节之后读取了20000个字节。我们可以在与FileStream再次交互之前调用ReadByte 19999次。

类似于这个示例,将BufferedStream与FileStream组合并没有太大我价值,因为FileStream已经具有内建的缓冲。他唯一的用处也许就是在已经构建的FileStream上增大缓冲。

关闭BufferedStream会自动关闭底层的后端存储流。

流适配器

Stream只能以字节方式进行处理;要读取或是写入例如字符串,整数或是XML元素这样的数据类型,我们必须借助于适配器。下面是框架所提供的适配:

  • 文本适配器(用于字符串与字符数据):TextReader,TextWriter,StreamReader,StreamWriter,StringReader,StringWriter
  • 二进制适配器(用于基础数据类型,例如int,bool,string与float):BinaryReader,BinaryWriter
  • XML适配器:XmlReader,XmlWriter

这些类型之间的关系如图14-5所示:

csharp_14_5.png

csharp_14_5.png

文本适配器

TextReader与TextWriter是用于处理字符与字符串的适配器的抽象基类。在框架中,每一个都有两个通用目的的实现:

  • StreamReader/StreamWriter:使用Stream作为原始的数据源,将流字节转换为字符或是字符串
  • StringReader/StringWriter:使用内存字符串实现了TextReader/TextWriter

表14-2分类列表了TextReader的成员。Peek返回流中的下一个字符,而不会改变位置。如果到达流的末尾,Peek与Read的零参数版本都会返回-1;否则,他们返回一个可以直接转换为char的整数。接受char[]缓冲的重载Read方法在功能上与ReadBlock方法相同。ReadLine读取直到遇到CR(字符13)或是LF(字符10)或是CR+LF对。然后他会返回一个字符串,丢弃CR/LF字符。

csharp_table_14_2.png

csharp_table_14_2.png

TextWriter具有用于写入的相似方法,如表14-3所示。Write与WriteLine方法都被重载来失道寡助任意的基本类型,以及object类型。这些方法会在所传递进来的对象上调用ToString方法(或者是通过在调用方法或是构建TextWriter时指定的IFormatProvider)。

csharp_table_14_3.png

csharp_table_14_3.png

WriteLine会简单的为所指定的文本添加CR+LF。我们可以通过NewLine属性来修改(这对于Unix文件格式交互时十分有用)。

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

这同时演示了如何测试文件的结尾(通过reader.Peek())。另一种方法就是一直读取直到reader.ReadLine返回null。

我们也可以读取或是写入其他的类型,例如整数,但是因为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类型则连接到底层面向字节的流,所以他们必须在字符与字节之间进行转换。他们是通过System.Text名字空间听 Encoding类来完成的,我们可以在构建StreamReader或是StreamWriter时选择。如果我们没有选择,则使用默认的UTF-8编码。

最简单的编码是ASCII编码,因为每一个字符由一个字节表示。ASCII编码将Unicode集合中的前127个字符映射为单个字节,转换我们在US风格的键盘上所看到的字符。大多数其他的字符,包括特殊符号以及非英语字符不能被表示,并被转换为□字符。默认的UTF-8编码可以映射所有的Unicode字符,但是他更为复杂。为了与ASCII兼容,前127个字符被编码为单个字节;其余的字符被编码为变化的字节数(通常是两个或是三个)。例如:

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”之后并不是一个标准的连字符,而是一个更长的em dash字符(—),U+2014。这不会使得我们的书本编辑器遇到麻烦。让我们看一下其输出:

98     // b
117    // u
116    // t
226    // em dash byte 1       Note that the byte values
128    // em dash byte 2       are >= 128 for each part
148    // em dash byte 3       of the multibyte sequence.
13     // <CR>
10     // <LF>

由于em dash位于Unicode集合中前127个字符之外,他要求更多个的字节来进行UTF-8编码(在这个示例中为三个)。UTF-8足够表示西方字符,因为大多数字符仅需要一个字节。通过简单的忽略127以上的字符,他可以很容易的转换为ASCII字符。其缺点是在流中定位比较麻烦,因为一个字符的位置并不与流中其字节位置相对应。另一种方法是UTF-16。下面是我们使用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 1
254    // Byte-order mark 2
98     // 'b' byte 1
0      // 'b' byte 2
117    // 'u' byte 1
0      // 'u' byte 2
116    // 't' byte 1
0      // 't' byte 2
20     // '--' byte 1
32     // '--' byte 2
13     // <CR> byte 1
0      // <CR> byte 2
10     // <LF> byte 1
0      // <LF> byte 2

由技术上来说,UTF-16为每个字符使用两个或是四个字节((有近100万个Unicode字符或是保留的,所以2个字节有时并不够)。然而,因为C# char类型本身是16位宽,UTF-16编码为每一个.NET char使用两个字节。这使得在流中很容易定位到一个特定的字符索引。

UTF-16使用两个字节前缀来表示字节对是以“小端”顺序还是“大端”顺序来编写。对于基于Windows的系统则为小端顺序。

StringReader与StringWriter

StringReader与StringWriter并没有封装流;相反,他们使用字符串或是StringBuilder作为底层数据存储。这就意味着并不需要字节转换-事实上,除了我们使用字符串或是StringBuilder结合索引变量很容易实现的事情以外,这个类并不能做其他事情。他们的优点是与StreamReader/StringWriter共享相同的基类。例如,假设我们有一个包含XML的字符串,并且希望使用XmlReader进行分析。XmlReader.Create可以接受下列中的一个:

  • URI
  • Stream
  • TextReader

那么我们如何使用XML分析我们的字符串?因为StringReader是TextReader的一个子类,则我们可以实例化并传递一个StringReader,如下所示:

XmlReader r = XmlReader.Create (new StringReader (myString));

二进制适配器

BinaryReader与BinaryWriter可以读取与写入本地数据类型:bool,byte,char,decimal,float,double,short,int,long,sbyte,ushort,unit与ulong,以及string和基础数据类型的数组。

与StreamReader和StreamWriter不同,二进制适配器可以高效的存储基础数据类型,因为他们在内存中表示。所以,一个int使用四个字节;一个double使用八个字节。字符串通过文本编码进行输出(类似于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. 关闭适配器,然后关闭流
  3. (对于写入)冲刷适配器(输出缓冲),然后关闭流
  4. (对于读取)仅关闭流

选项1与2在语义是上相同的,因为关闭适配器会自动关闭底层流。当我们嵌入using语句时,我们隐式的选择了选项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的缓冲被写入底层流中。

文件与目录操作

System.IO名字空间提供了一个执行文件与目录操作的类型集合,例如拷贝与移动,创建目录,以及设置文件的属性与权限等。对于大多数特性,我们可以在两个类之间进行选择,一个提供了静态方法而另一个提供了实例化方法:

  • 静态类:File与Directory
  • 实例方法类(使用文件或是目录名构建):FileInfo与DirectoryInfo

另外,还有一个名为Path的静态类。这个类对于文件或是目录并没有什么;相反,他为文件名与目录路径提供了字符串处理的方法。Path同时辅助临时文件处理。

File类

File是一个静态类,其方法接受文件名。文件名可以相对于当前目录或是具有目录的绝对路径。该类所具有的方法如下:

bool Exists (string path);      // Returns true if the file is present
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);      // UTC versions are
DateTime 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会抛出异常;Replace则不会。两个方法都允许文件被重命名以及移动到另一个目录中。

如果文件被标记为只读,则会抛出UnauthorizedAccessException;如果我们通过调用GetAttributes来识别属性。下面是GetAttributes返回的FileAttribute枚举成员:

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

这个枚举中的成员是可组合的。下面显示如何修改文件的一个属性而不影响其他的属性:

string filePath = @"c:\temp\test.txt";
FileAttributes fa = File.GetAttributes (filePath);
if ((fa & FileAttributes.ReadOnly) > 0)
{
    fa ^= FileAttributes.ReadOnly;
    File.SetAttributes (filePath, fa);
}
// Now we can delete the file, for instance:
File.Delete (filePath);

压缩与加密属性

Compressed与Encrypted文件属性对应于Windows文件管理器中文件或是目录属性对话框中的压缩与加密复选框。这种压缩与加密类型是透明的,因为操作系统会在幕后完成所有的工作,使得我们可以读取与写入普通数据。

我们不能使用SetAttributes来修改文件的Compressed或是Encrypted属性-如果我们这样做则会静默失败。在后一种情况中工作过程很简单:我们调用File类中的Encypt()与Decrypt()方法。对于压缩,他则更为复杂;一种解决方法就是使用System.Management中的Windows管理工具(WMI)。下面的代码压缩一个目录,如果成功则返回0(或者是失败时返回一个WMI代码):

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替换为UncompressEx。

透明加密依赖于由登陆用户的密码生成的键。系统对于验证用户所做的密码修改是健壮的,但是如果密码被管理员重置,加密文件中的数据则不可恢复。

我们可以使用Win32交互是确定一个卷是否支持压缩与加密:

using System;
using System.IO;
using System.Text;
using System.Runtime.InteropServices;
class SupportsCompressionEncryption
{
  const int SupportsCompression = 0×10;
  const int SupportsEncryption = 0×20000;
  [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;
  }
}

文件安全

GetAccessControl与SetAccessControl方法可以使得我们查询并修改通过FileSecurity对象(System.Security.AccessControl名字空间)赋给用户与角色的操作系统权限。我们也可以在创建新文件时向FileStream的构造器传递FileSecurity来指定权限。

在这个示例中,我们列出文件已存在的权限,然后向“Users”赋予执行权限:

using System;
using System.IO;
using System.Security.AccessControl;
using System.Security.Principal;
...
FileSecurity sec = File.GetAccessControl (@"c:\temp\test.txt");
AuthorizationRuleCollection rules = sec.GetAccessRules (true, true,
                                                     typeof (NTAccount));
foreach (FileSystemAccessRule rule in rules)
{
  Console.WriteLine (rule.AccessControlType);         // Allow or Deny
  Console.WriteLine (rule.FileSystemRights);          // e.g., FullControl
  Console.WriteLine (rule.IdentityReference.Value);   // e.g., MyDomain/Joe
}
FileSystemAccessRule newRule = new FileSystemAccessRule
  ("Users", FileSystemRights.ExecuteFile, AccessControlType.Allow);
sec.AddAccessRule (newRule);
File.SetAccessControl (@"c:\temp\test.txt", sec);

Directory类

静态Directory类提供了与File类中的方法类似的方法集合-用于检测目录是否存在(Exists),移动目录(Move),删除目录(Delete),读取/设置创建或是上次访问的时间,以及读取/设置安全权限。而且,Diretory提供了下列静态方法:

string GetCurrentDirectory ();
void   SetCurrentDirectory (string path);
DirectoryInfo CreateDirectory  (string path);
DirectoryInfo GetParent        (string path);
string        GetDirectoryRoot (string path);
string[] GetLogicalDrives();
// 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);

Enumerate*与Get*方法被重载同时接受searchPaatern(字符串)与searchOption(枚举)参数。如果我们指定了SearchOption.SearchAllSubDirecotries,则会执行递归的子目录搜索。*FileSystemEntries方法组合了*Files与*Directories的结果。

下面的代码演示了如果目录不存在如何创建目录的例子:

if (!Directory.Exists (@"c:\temp"))
  Directory.CreateDirectory (@"c:\temp");

FileInfo与DirectoryInfo

File与Directory的静态方法对于执行单个的文件或是目录操作十分方便。如果我们希望调用一个方法序列,FileInfo与DirectoryInfo类提供了一个对象模式,使得这一工作更为简单。

FileInfo以实例形式提供了File静态方法中的大多数,同时还有其他的属性,例如Extension,Length,IsReadOnly以及用于返回DirectoryInfo对象的Directory。例如:

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 (still)
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;   // (Toggle hidden flag)
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来枚举文件与子目录:

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类为处理路径与文件名定义了方法与域。假定下面的设置代码:

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

我们可以使用下面的表达式来演示Path的方法与域:

csharp_path.png

csharp_path.png

Combine特别有用:他使得我们可以组合目录与文件名或是两个目录,而不是需要检测第一个目录名是否有结尾的斜线。

GetFullPath将相对于当前目录的路径转换为绝对路径。他接受类似于”..\..\file.txt”这样的值。

GetRandomFileName返回一个8.3字符的文件名,而不会实创建文件。GetTempFileName会使用一个在65000文件上重复的自增计数器来生成一个临时文件名,然后在本地临时目录中创建这个这个名字的空文件。

特殊目录

Path与Directory中缺少一种定位例如My Documnts,Program Files,Application Data这样的目录的方法。这是由System.Environment类中的GetFolderPath方法提供的:

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

Environment.SpecialFolder是一个枚举,其中包含了Windows中的所有特殊目录:

csharp_specialfolder.png

csharp_specialfolder.png

其中的一个特殊值就是ApplicationData:这是存储随用户跨网络移动的设置的地方(如果在网络域上允许了漫游配置)LocalApplicationData用于非漫游数据;CommonApplicationData为该计算机上的所有用户共享。比起使用Windows注册表,将程序数据写入这些目录被认为是更好的方式。在大多数情况下,更好的解决方法仍然是使用独立存储(我们会在本章的最后部分进行描述)。

下面的代码返回.NET框架的目录:

System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory()

查询卷信息

我们可以使用DriveInfo类查询计算机上的驱动器信息:

DriveInfo c = new DriveInfo ("C");       // Query the C: drive.
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.
{
  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,存储卡以及网络连接。DriveType是一个枚举,其值如下:

Unknown, NoRootDirectory, Removable, Fixed, Network, CDRom, Ram

捕获文件系统事件

FileSystemWatcher类可以使得我们监视目录(子目录)的活动。FileSystemWatcher类具有当文件或是子目录被创建,修改,重命名,删除,以及他们的属性发生变化时可以触发的事件。这些事件被触发而无论执行引起该变化的用户或进程。如下面的示例:

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();
  }
  // Disposing the FileSystemWatcher stops further events from firing.
}
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);
}

Error事件并不会通知我们文件系统的错误;相反,他表明FileSystemWatcher的事件缓冲区溢出,因为他被Changed,Created,Deleted或是Renamed事件所填满。我们可以通过InternalBufferSize属性来修改缓冲区的尺寸。

IncludeSubdirectories会递归应用。所以如果我们在C:\上创建一个FileSystemWatcher并且IncludeSubdirectories为true,从而当磁盘上任意位置的文件或是目录发生变化时,其事件都会被触发。

内存映射文件(Memory-Mapped Files)

内存映射文件是由框架4.0新引入的。他们提供了两个关键特性:

  • 对于文件数据的高效随机访问
  • 在相同计算不同进程之间共享内存的能力

内存映射文件的类型位于Systme.IO.MemoryMappedFiles名字空间中。在内部,他们是通过封装用于内存映射文件的Win32 API来工作的。

内存映射文件与随机文件IO

尽管普通的FileStream允许随机文件IO(通过设置流的Position属性),他却为顺序IO进行了优化。基本原则如下:

  • 对于顺序IO,FileStream要10倍于内存映射文件
  • 对于随机IO,内存映射文件要10倍于FileStream

改变FileStream的Position属性需要花费几毫秒的时间-在循环中会进行累加。FileStream也不适用于多线程访问-因为在读取或是写入时其位置会发生变化。

要创建内存映射文件,我们可以:

  1. 像通常那样获取获取FileStream。
  2. 传递文件流,实例化MemoryMappedFile。
  3. 在内存映射文件对象上调用CreateViewAccessor。

最后一步会为我们返回一个MemoryMappedViewAccessor对象,他提供了随机读写简单类型,结构,与数组的方法。

下面的代码创建了一个一百万字节的文件,然后使用内存映射API进行读取并在位500000处写入一个字节:

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时指定映射名与容量。指定一个非空的映射名可以使得内存块为其他的进程所共享;指定容量会自动将文件扩展到该容量值。下面的代码创建了一个1000字节的文件:

using (var mmf = MemoryMappedFile.CreateFromFile
                 ("long.bin", FileMode.Create, null, 1000))
  ...

内存映射文件与共享内存

我们可以将内存映射文件用作相同计算机上进程之间共享内存的一种方法。一个进程通过调用MemoryMappedFile.CreateNew来创建一个共享的内存块,而其他的进程通过相同的名字来调用MemoryMappedFile.OpenExisting方法来订阅相同的内存块。尽管他仍然作为内存映射文件来引用,但是他完全存在于内存之中,并不占用磁盘。

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

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 EXE:
using (MemoryMappedFile mmFile = MemoryMappedFile.OpenExisting ("Demo"))
using (MemoryMappedViewAccessor accessor = mmFile.CreateViewAccessor())
  Console.WriteLine (accessor.ReadInt32 (0));   // 12345

使用视力访问器

在MemoryMappedFile上调用CreateViewAccessor方法会为我们返回一个可以在随机位置读写值的视力访问器。

Read*/Write*方法接受数值类型,bool,char,以及包含值类型元素或是域的数组与结构。引用类型以及包含引用类型的数组与结构是被禁止的,因为他们不能映射到未托管的内存中。如果我们要写入一个字符串,我们必须将其编码为一个字节数组:

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;
  accessor.SafeMemoryMappedViewHandle.AcquirePointer (ref pointer);
  int* intPointer = (int*) pointer;
  Console.WriteLine (*intPointer);               // 123
}

指针在处理大结构时很有优势:可以使得我们直接处理原始数据而不是使用Read/Write来在托管与非托管内存之间拷贝数据。我们会在第25章中进一步探讨。

压缩

两个通用目的的压缩流是在System.IO.Compression名字空间中提供的:DeflateStream与GZipStream。两者都使用类似于ZIP格式的流行压缩算法。他们之间的不同之处在于GZipStream会在起始与结束处添加额外的协议,包括用于错误检测的CRC。GZipStream同时遵循其他软件可以识别的标准。

两个流都允许读取与写入,但有下面的限制:

  • 当压缩时我们总是写入流
  • 当解压时我们总是读取流

DeflateStream与GZipStream是装饰器;他们压缩或是解压我们在构建时所提供的其他流中的数据。在下面的示例中,我们压缩并解压一系列字节,使用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

即使是两个算法中较小的一个,压缩后的文件也有241字节长:是原始数据的2倍多。压缩对于“密集”的非重复的二进制文件处理效果较差。他对于大多数的文本文件处理效果都较好。在下面的示例中,我们压缩并解压一个在一段文章中随机选取的1000个单词组成的文本流。这个示例同时演示了链接后端存储流,装饰器流以及适配器:

string[] words = "The quick brown fox jumps over the lazy dog".Split();
Random rand = new Random();
using (Stream s = File.Create ("compressed.bin"))
using (Stream ds = new DeflateStream (s, CompressionMode.Compress))
using (TextWriter w = new StreamWriter (ds))
  for (int i = 0; i < 1000; i++)
    w.Write (words [rand.Next (words.Length)] + " ");
Console.WriteLine (new FileInfo ("compressed.bin").Length);      // 1073
using (Stream s = File.OpenRead ("compressed.bin"))
using (Stream ds = new DeflateStream (s, CompressionMode.Decompress))
using (TextReader r = new StreamReader (ds))
  Console.Write (r.ReadToEnd());                 // Output below:
lazy lazy the fox the quick The brown fox jumps over fox over fox The
brown brown brown over brown quick fox brown dog dog lazy fox dog brown
over fox jumps lazy lazy quick The jumps fox jumps The over jumps dog...

在这个例子中,DeflateStream高效的压缩为1073字节-仅比每一个单词多一个字节。

在内存中压缩

有时我们需要在全部在内存中进行压缩。下面的代码演示了如何使用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);       // 113
// 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));

DeflatStream周围的using语句以规范的方式关闭,在此过程中输出未写入的缓冲区。这同时会关闭其所封装的MemoryStream-意味着我们必须调用ToArray来解出数据。

下面是避免关闭MemoryStream的另一种方法:

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

发送给DefaltStream构造器的额外标记通知他不要遵循在销毁时销毁底层数据流的通常协议。换句话说,MemoryStram保持打开,使得我们可以将其定位到零并重新读取。

隔离存储

每一个.NET程序都可以访问对于程序唯一的特殊操作文件系统,名为隔离存储。隔离存储十分有用与重要,原因如下: