.NET Framework 4 基类库中的新增内容

使用 Microsoft .NET 的每个人几乎都会用到基类库 (BCL)。一个更好的 BCL 几乎可以惠及每一位托管代码开发人员。 此专栏重点介绍 .NET 4 Beta 1 在 BCL 中新增的功能。

先前的文章中已经介绍了其中的三项新增功能 — 首先,让我们简单回顾一下:

  • 支持代码约定
  • 并行扩展(任务、并发集合和协调数据结构)
  • 支持元组 
    接下来,本文将主要介绍另外三项新增功能:
  • 文件 IO 改进
  • 支持内存映射文件
  • 经过排序的集集合

由于篇幅所限,本文无法介绍所有最新的 BCL 改进,不过,BCL 团队博客很快会在 blogs.msdn.com/bclteam 上发表相关文章,您可以一睹为快。 这些改进包括:

  • 支持任意大整数
  • 接口和委托上的泛型方差批注
  • 允许访问 32 位和 64 位注册表视图以及创建可变注册表项
  • 全球化数据更新
  • 改进了 System.Resourcesresource 查找回退逻辑
  • 压缩改进

我们还计划介绍 Beta 2 的部分其他功能和改进,大约在 Beta 2 推出时,您可以在 BCL 团队博客中了解这些功能和改进的相关信息。

代码约定

在 .NET Framework 4 中,BCL 的一个主要新增功能是代码约定。 利用这个新库,您可以通过一种与语言无关的方法在代码中指定前置条件、后置条件和对象不变量。 Melitta Andersen 在 2009 年 8 月期 MSDN 杂志的“CLR 全面透析”专栏中,对代码约定进行了详细介绍。 您还应该看一看代码合同 DevLabs 站点,在msdn.microsoft.com/devlabs/dd491992.aspx 和在 blogs.msdn.com/bclteam BCL 团队博客上。

并行扩展

随着多核处理器在客户端更为重要以及大规模并行服务器的应用更为广泛,帮助程序员轻松使用所有这些处理器显得比以往任何时候都重要。 在 .NET 4 中,BCL 的另外一项主要新增功能是并行扩展 (PFX) 功能,该功能由并行计算平台团队提供。 PFX 包括任务并行库 (TPL)、协调数据结构、并发集合和并行 LINQ (PLINQ) — 在编写可利用多核计算机的代码时,这些功能可以简化此类代码的编写。 可以在 Stephen Toub 和 Hazim Shafi 文章中找到更多背景上 PFX"改进的支持并行度,在下一版本的 Visual Studio 中的"在 2008 年 10 月联机 msdn.microsoft.com/magazine/cc817396.aspx 在可用的 MSDN Magazine的问题。 blogs.msdn.com/pfxteam PFX 团队博客也是很好的 PFX 信息源。

元组

在 .NET 4 中,BCL 的另一项新增功能是支持元组,元组类似于可以动态创建的匿名类。 元组是很多功能语言和动态语言(如 F# 和 Iron Python)中使用的一种数据结构。 通过在 BCL 中提供通用元组类型,有助于更好地实现语言互操作性。 许多程序员发现元组使用起来非常方便,尤其是从方法返回多个值时更是如此。因此,就连 C# 或 Visual Basic 开发人员也会发现它们很有用。 在 2009 年 7 月期 MSDN 杂志的“CLR 全面透析”专栏中,Matt Ellis 介绍了 .NET 4 中新增的对元组的支持。

文件 IO 改进

我们未详细介绍的一个 .NET 4 新增功能是 System.IO.File 中新增的用于读取和写入文本文件的方法。 自 .NET 2.0 以来,如果需要读取文本文件中的行,可以调用 File.ReadAllLines 方法,该方法以字符串数组的形式返回文件中的所有行。 下面的代码使用 File.ReadAllLines 读取文本文件中的行,并将行的长度及行本身写入控制台:

string[] lines = File.ReadAllLines("file.txt");
foreach (var line in lines) {
Console.WriteLine("Length={0}, Line={1}", line.Length, line);
}

遗憾的是,上面的代码存在一个不易察觉的问题。 这个问题是由于 ReadAllLines 返回数组造成的。ReadAllLines 必须读取所有行并分配要返回的数组,然后才能返回。 对于相对较小的文件而言,这并不太糟,但对于包含数百万行的大文本文件,就会产生问题。 假设要打开一个文本文件格式的电话簿或 900 页小说。 在将所有行加载到内存中之前,ReadAllLines 始终处于阻止状态。 这不仅导致内存使用效率低下,还会延迟对行的处理,因为在所有行都读入内存之后,才能访问第一行。

要解决这个问题,可以使用 TextReader 打开此文件,然后将文件的行逐一读入内存。 尽管该方法行之有效,但并不像调用 File.ReadAllLines 那样简单:

using (TextReader reader = new StreamReader("file.txt")) {
string line;
while ((line = reader.ReadLine()) != null) {
Console.WriteLine("Length={0}, Line={1}", line.Length, line);
}
}

在 .NET 4 中,我们为 File 新增了一个名为 ReadLines 的方法(相对于 ReadAllLines),该方法返回 IEnumerable<string> 而不是 string[]。 这个新增方法的效率要高很多,因为它不是将所有行一次性加载到内存中,而是每次读取一行。 下面的代码使用 File.ReadLines 高效读取文件中的行,使用起来和效率较低的 File.ReadAllLines 一样方便:

IEnumerable<string> lines = File.ReadLines(@"verylargefile.txt");
foreach (var line in lines) {
Console.WriteLine("Length={0}, Line={1}", line.Length, line);
}

请注意,对 File.ReadLines 的调用会立即返回。 您不必再等所有行都读入内存,即可对行进行循环访问。 实际上,文件的读取操作是在 Foreach 循环迭代的驱动之下进行的。 由于可以在读取行的同时开始处理行,因此该方法显著提高了可感知的代码性能;此外,由于每次只读取一行,因此还大大提高了效率。 使用 File。ReadLines 的另一个新增功能是允许在需要时提前中止循环,而不必浪费时间读取不需要的其他行。

我们还为 File.WriteAllLines 新增了一些重载方法,这些重载方法采用 IEnumerable<string> 参数,与采用 string[] 参数的现有重载方法类似。 此外,我们还新增了一个名为 AppendAllLines 的方法,该方法使用 IEnumerable<string> 参数向文件追加行。 利用这些新增的方法,不必传入数组,就可以方便地向文件写入或追加行。 这意味着,如果有一个字符串集合,可以直接将它传递给这些方法,而无需先将其转换为字符串数组。

在 BCL 中,使用 IEnumerable<T>(而非数组)的好处不止于此。 以文件系统枚举 API 为例。 在以往的 Framework 版本中,要获取目录中的文件,需要调用 DirectoryInfo. GetFiles 等方法,此类方法可返回 FileInfo 对象数组。 然后,可以循环访问 FileInfo 对象以获取关于文件的信息,例如每个文件的名称和长度。具体代码如下所示:

DirectoryInfo directory = new DirectoryInfo(@"\\share\symbols");
FileInfo[] files = directory.GetFiles();
foreach (var file in files) {
Console.WriteLine("Name={0}, Length={1}", file.Name, file.Length);
}

上面的代码存在两个问题。 第一个问题不会令您感到吃惊,因为与 File.ReadAllLines 的问题一样,此问题是由于 GetFiles 返回数组造成的。 GetFiles 必须从文件系统中检索目录中的完整文件列表并分配要返回的数组,然后才能返回。 这意味着,必须在检索了所有文件之后,才能获得第一批结果,这种情况下,内存的使用率很低。 如果目录包含一百万个文件,就必须先检索全部一百万个文件并分配长度为一百万的数组。

上面的代码还有一个更为隐蔽的问题。 FileInfo 实例是通过将文件路径传递给 FileInfo 的构造函数创建的。FileInfo 的各个属性(如 Length 和 CreationTime)是在首次访问 FileInfo 的某个属性时初始化的。 首次访问某个属性时,会调用 FileInfo.Refresh,从而调用操作系统从文件系统中检索文件的各个属性。 这样,就避免了在属性从未使用时进行调用来检索数据,如果使用了属性,则有助于确保在首次访问时数据未过时。 对于 FileInfo 的一次性实例,这非常有用,但当枚举目录中的内容时,可能会产生问题,因为这意味着将多次调用文件系统来获取文件属性。 遍历结果时,进行多次调用会影响性能。 如果要枚举远程文件共享的内容,此问题尤为突出,因为这意味着通过网络对远程计算机进行再一次往返调用。

在 .NET 4 中,我们已经解决了这两个问题。 为了解决第一个问题,我们在 Directory 和 DirectoryInfo 中添加了一些新方法,这些方法将返回 IEnumerable<T> 而非数组。

与 File.ReadLines 一样,这些基于 IEnumerable<T> 的新方法比基于数组的旧方法更高效。 下面的代码已更新为使用 .NET 4 的 DirectoryInfo. EnumerateFiles 方法而非 DirectoryInfo.GetFiles:

DirectoryInfo directory = new DirectoryInfo(@"\\share\symbols");
IEnumerable<FileInfo> files = directory.EnumerateFiles();
foreach (var file in files) {
Console.WriteLine("Name={0}, Length={1}", file.Name, file.Length);
}

与 GetFiles 不同,EnumerateFiles 不必在检索完所有文件之前一直处于阻止状态,也不必分配数组。 相反,它将立即返回,从文件系统返回每一个文件时,可以对其进行处理。

为解决第二个问题,DirectoryInfo 现在使用操作系统在枚举法。 实际上,DirectoryInfo 在枚举过程中为获得文件系统内容而调用的基础 Win32 函数包含有关每个文件的数据,例如长度和创建时间。 现在,在初始化通过 DirectoryInfo 的基于数组的旧方法和基于 IEnumerable<T> 的新方法返回的 FileInfo 和 DirectoryInfo 实例时,我们将使用这些数据。 这意味着,在前面的代码中,当调用 file.Length 时,由于该数据已初始化,因此不对文件系统进行任何多余基础调用即可检索文件长度。

总之,通过 File 和 Directory 的基于 IEnumerable<T> 的新方法,可以实现一些有用的方案。 请考虑以下代码:

var errorlines =
from file in Directory.EnumerateFiles(@"C:\logs", "*.log")
from line in File.ReadLines(file)
where line.StartsWith("Error:", StringComparison.OrdinalIgnoreCase)
select string.Format("File={0}, Line={1}", file, line);
File.WriteAllLines(@"C:\errorlines.log", errorlines);

这段代码使用 Directory 和 File 的新方法以及 LINQ,高效查找扩展名为 .log 的文件,并在这些文件中查找以“Error:”开头的行。 然后,该查询将结果投影到新的字符串序列中,每个字符串都进行格式化以显示文件路径和错误行。 最后,使用 File. WriteAllLines 将错误行写入名为“errorlines.log”的新文件,而不必将错误行转换为数组。 该代码的最大优点在于非常高效。 我们无需将整个文件列表读入内存,也无需将文件的全部内容读入内存。 无论 C:\logs 包含 10 个文件还是 100 万个文件,无论文件包含 10 行还是 100 万行,上面的代码都会使用尽量少的内存高效执行。

内存映射文件

对内存映射文件的支持是 .NET Framework 4 中的另一项新增功能。内存映射文件可用于编辑大文件或为进程间通信 (IPC) 创建共享内存。 内存映射文件可用于编辑大文件或为进程间通信 (IPC) 创建共享内存。 通过内存映射文件,可以将文件映射到进程的地址空间。 映射之后,应用程序通过对内存进行读写操作,即可访问或修改文件的内容。 因为文件是通过操作系统的内存管理器访问的,所以文件自动分区到很多页面中,页面则根据需要换入或换出内存。 因为不必自己进行内存管理,使得大文件的处理更为方便。 在这种方式下,还可以对文件进行完全随机访问,而无需进行查找。

内存映射文件可在没有支持文件的情况下创建。 这类内存映射文件由系统分页文件支持(仅当存在系统分页文件并且内容需要换出内存时)。 内存映射文件可供多个进程共享,这意味着,通过内存映射文件为进程间通信创建共享内存是一种很好的方法。 每个映射都可拥有一个与自己关联的名称,以便其他进程用来打开该内存映射文件。

要使用内存映射文件,必须首先使用 System.IO.MemoryMappedFiles.MemoryMappedFile 类的以下某个静态工厂方法创建 MemoryMappedFile 实例:

  • CreateFromFile
  • CreateNew
  • CreateOrOpen
  • OpenExisting

然后,可以创建一个或多个视图,将文件实际映射到进程地址空间中。 每个视图都可映射全部或部分内存映射文件,并且视图可以重叠。

如果文件的大小大于进程可用于映射的逻辑内存空间(在 32 位计算机中为 2GB),就需要使用多个视图。通过调用 MemoryMappedFile 对象的 CreateViewStream 或 CreateViewAccessor 方法,可以创建视图。CreateViewStream 返回 MemoryMappedFileViewStream 实例,该实例继承自 System.IO.UnmanagedMemoryStream。 它可以像 Framework 中的任何其他 Stream 一样使用。 另一方面,CreateViewAccessor 返回 MemoryMappedFileViewAccessor 实例,该实例继承自新增的 System.IO.UnmanagedMemoryAccessorclass。 UnmanagedMemoryAccessor 支持随机访问,而 UnmanagedMemoryStream 支持顺序访问。

新增方法...

... System.IO.file 上

  • public static IEnumerable<string>ReadLines(string path)
  • public static void WriteAllLines(string path, IEnumerable<string> contents)
  • public static void AppendAllLines(string path, IEnumerable<string> contents)

... System.IO.directory 上

  • public static Enumerable<string>EnumerateDirectories(string path)
  • public static IEnumerable<string>EnumerateFiles(string path)
  • public staticIEnumerable<string>EnumerateFileSystemEntries(string path)

... System.IO.DirectoryInfo 上

  • publicIEnumerable<DirectoryInfo>EnumerateDirectories()
  • publicIEnumerable<FileInfo>EnumerateFiles()
  • publicIEnumerable<FileSystemInfo>EnumerateFileSystemInfos()

下面的示例说明如何使用内存映射文件为 IPC 创建共享内存。 进程 1(如图 1 所示)通过在 CreateNew 方法中指定内存映射文件的名称和容量(以字节为单位),创建一个新的 MemoryMappedFile 实例。 这样将创建一个由系统分页文件支持的内存映射文件。 请注意,在系统内部,指定的容量会向上取整为下一个系统页面大小倍数(如果您感兴趣,可以从 .NET 4 中新增的 Environment.System- PageSize 获取系统页面大小)。 接下来,使用 CreateViewStream 创建一个视图流,并使用 BinaryWriter 的实例将“Hello Word!”写入该流中。 然后,启动第二个进程。

进程 2(如图 2 所示)通过指定适当的内存映射文件名称,使用 OpenExisting 方法打开现有的内存映射文件。 然后,创建一个视图流并使用 BinaryReader 的实例读取上述字符串。

图 1 进程 1

using (varmmf = MemoryMappedFile.CreateNew("mymappedfile", 1000))
using (var stream = mmf.CreateViewStream()) {
var writer = new BinaryWriter(stream);
writer.Write("Hello World!");
varstartInfo = new ProcessStartInfo("process2.exe");
startInfo.UseShellExecute = false;
Process.Start(startInfo).WaitForExit();
}

图 2 进程 2

using (varmmf = MemoryMappedFile.OpenExisting("mymappedfile"))
using (var stream = mmf.CreateViewStream()) {
var reader = new BinaryReader(stream);
Console.WriteLine(reader.ReadString());
}

SortedSet<T>

除了 System.Collections.Concurrent(PFX 的组成部分)中新增的集合外,.NET Framework 4 还在 System.Collections.Generic 中提供了一个新的集集合,称为 SortedSet<T>。 与 .NET 3.5 中新增的 HashSet<T> 一样,SortedSet<T> 也是由唯一元素组成的集合,但与 HashSet<T> 不同,SortedSet<T> 的元素按排序顺序排列。

SortedSet<T> 是使用自平衡红黑树实现的,其插入、删除和查找操作的性能复杂度为 O(log n)。 相比而言,HashSet<T> 的插入、删除和查找操作的性能稍好,复杂度为 O(1)。 如果只需常规用途的集,大多数情况下应使用 HashSet<T>。 但是,如果需要按排序顺序排列的元素、获取特定范围内的元素子集,或者获取最小或最大元素,则应使用 SortedSet<T>。 以下代码说明如何使用 SortedSet<T> 处理整数:

var set1 = new SortedSet<int>() { 2, 5, 6, 2, 1, 4, 8 };
bool first = true;
foreach (var i in set1) {
if (first) {
first = false;
}
else {
Console.Write(",");
}
Console.Write(i);
}
// Output: 1,2,4,5,6,8

该集是使用 C# 的集合初始值设定项语法创建和初始化的。 请注意,整数不是以特定顺序添加到集中的。 另外请注意,添加了两次 2。 显然,遍历 set1 的元素时,整数以排序顺序排列,并且该集仅包含一个 2。 与 HashSet<T> 一样,SortedSet<T> 的 Add 方法返回布尔类型的值,用于确定是成功添加了项 (true) 还是因集中已有该项而未能添加 (false)。

图 3 说明如何获取集中的最大和最小元素,以及如何获取特定范围内的元素子集。

图 3 获取元素的最大、最小值和子集视图

var set1 = new SortedSet<int>() { 2, 5, 6, 2, 1, 4, 8 };
Console.WriteLine("Min: {0}", set1.Min);
Console.WriteLine("Max: {0}", set1.Max);
var subset1 = set1.GetViewBetween(2, 6);
Console.Write("Subset View: ");
bool first = true;
foreach (var i in subset1) {
if (first) {
first = false;
}
else {
Console.Write(",");
}
Console.Write(i);
}
// Output:
// Min: 1
// Max: 8
// Subset View: 2,4,5,6

GetViewBetween 方法返回原始集的视图。 这意味着对视图所做的任何更改都将反映在原始视图中。 例如,如果在上述代码中将 3 添加到 subset1,实际上会添加到 set1。 请注意,不能将项添加到指定范围之外的视图。 例如,如果试图在上述代码中将一个 9 添加到 subset1,则会产生 Argument-Exception,因为视图在 2 和 6 之间。

原文链接:http://msdn.microsoft.com/zh-cn/magazine/ee428166.aspx

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值