26.1.3 文件I/O的改进

本节将以对比的方式介绍.NET 4对文件I/O所做的改进。

首先介绍System.IO.File中用于读取和写入文本文件的方法。自.NET 2.0以来,如果需要读取文本文件中的行,可以调用File.ReadAllLines方法,该方法以字符串数组的形式返回文件中的所有行。下面的代码使用File.ReadAllLines读取文本文件中的行,并将行的长度及行本身写入控制台,如下所示。


//操作的目录

string dirPath=@"c:\iosample";

//操作的文件

string filePath=string.Format(@"{0}{1}",dirPath,"test.txt");

string[]lines=File.ReadAllLines(filePath);


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

在.NET 4中,File类新增了一个名为ReadLines的方法,该方法返回IEnumerable<string>而不是string[]。这个方法的签名如下:


public static IEnumerable<string>ReadLines(string path)


这个新增方法的效率要高很多,因为它不是将所有行一次性加载到内存中,而是每次只读取一行。下面的代码使用File.ReadLines方法高效读取文件中的行,使用却和效率较低的File.ReadAllLines方法一样简单:


IEnumerable<string>lines2=File.ReadLines(filePath);


接下来分别使用这两种方法读取一个具有17 982行的文本文件,然后分别计时,代码如下:


using System;

using System.Collections.Generic;

using System.IO;

namespace ProgrammingCSharp4

{

class IOSample

{

public static void Main()

{

StopWatch sw=new StopWatch();

sw.Begin();

Console.WriteLine(“开始使用File.ReadAllLines读取”);

//操作的目录

string dirPath=@"c:\iosample";

//操作的文件

string filePath=string.Format(@"{0}{1}",dirPath,"test.txt");

string[]lines=File.ReadAllLines(filePath);

foreach(string line in lines)

{

}

Console.WriteLine(“读取使用时间:{0}”,sw.StopAndShowTime());

Console.WriteLine();

sw.Reset();

sw.Begin();

Console.WriteLine(“开始使用File.ReadLines读取”);

IEnumerable<string>lines2=File.ReadLines(filePath);

foreach(string line in lines2)

{

}

Console.WriteLine(“读取使用时间:{0}”,sw.StopAndShowTime());

}

}

class StopWatch

{

private int mintStart;

public void Begin()

{

mintStart=Environment.TickCount;

}

public long StopAndShowTime()

{

return Environment.TickCount-mintStart;

}

public void Reset()

{

mintStart=0;

}

}

}


上述代码的运行结果如下:


开始使用File.ReadAllLines读取

读取使用时间:203

开始使用File.ReadLines读取

读取使用时间:110

请按任意键继续……


从上述运行结果可以看出两者效率的差别。使用File.ReadLines的另一个新增功能是允许在需要时提前中止循环,而不必浪费时间读取不需要的其他行。

另外,File.WriteAllLines也新增了一些重载方法,这些重载方法采用IEnumerable<string>参数,与采用string[]参数的现有重载方法类似。

❑public static void WriteAllLines(string path,IEnumerable<string>contents)

❑public static void WriteAllLines(string path,IEnumerable<string>contents,Encoding encoding)

此外,还新增了一个名为AppendAllLines的方法,该方法使用IEnumerable<string>参数向文件追加行。

❑public static void AppendAllLines(string path,IEnumerable<string>contents)

❑public static void AppendAllLines(string path,IEnumerable<string>contents,Encoding encoding)

利用这些新增的方法,不必传入数组,就可以方便地向文件写入或追加行。这意味着,如果有一个字符串集合,可以直接将它传递给这些方法,而无须先将其转换为字符串数组。

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


//操作的目录

string dirPath=@"c:\windows\system32";

//操作的文件

DirectoryInfo directory=new DirectoryInfo(dirPath);

FileInfo[]files=directory.GetFiles();


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

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

在.NET 4中,这两个问题已经得到了解决。Directory和DirectoryInfo类中新添增了一些方法,这些方法将返回IEnumerable<T>类型而非数组。

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


IEnumerable<FileInfo>files2=directory.EnumerateFiles();


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

为解决第二个问题,DirectoryInfo在使用基于IEnumerable<T>的新方法返回的FileInfo和DirectoryInfo实例时,这些数据将在调用file.Length之前初始化,因此不仅对文件系统执行任何多余调用即可检索文件长度。因为基类FileSystemInfo新添加了一个内部方法InitializeFrom,如下:


internal void InitializeFrom(Win32Native.WIN32_FIND_DATA findData)

{

this._data=new Win32Native.WIN32_FILE_ATTRIBUTE_DATA();

this._data.PopulateFrom(findData);

this._dataInitialised=0;

}


该方法将在迭代返回每个FileInfo和DirectoryInfo实例时被调用,此方法中执行了属性数据的加载操作,数据将被加载到FileSystemInfo类的_data字段,FileInfo的Length属性访问器即是从_data获取文件的长度等数据。

接下来分别使用这两种方法读取C:\Windows\System32目录下的所有文件,然后分别计时,代码如下:


using System;

using System.Collections.Generic;

using System.IO;

namespace ProgrammingCSharp4

{

class IOSample

{

public static void Main()

{

StopWatch sw=new StopWatch();

sw.Begin();

Console.WriteLine(“开始使用directory.GetFiles()读取”);

//操作的目录

string dirPath=@"c:\windows\system32";

DirectoryInfo directory=new DirectoryInfo(dirPath);

FileInfo[]files=directory.GetFiles();

foreach(var file in files)

{

}

Console.WriteLine(“读取使用时间:{0}”,sw.StopAndShowTime());

Console.WriteLine();

sw.Reset();

sw.Begin();

Console.WriteLine(“开始使用directory.EnumerateFiles读取”);

IEnumerable<FileInfo>files2=directory.EnumerateFiles();

foreach(var file in files2)

{

}

Console.WriteLine(“读取使用时间:{0}”,sw.StopAndShowTime());

}

}

class StopWatch

{

private int mintStart;

public void Begin()

{

mintStart=Environment.TickCount;

}

public long StopAndShowTime()

{

return Environment.TickCount-mintStart;

}

public void Reset()

{

mintStart=0;

}

}

}


上述代码的运行结果如下:


开始使用directory.GetFiles()读取

读取使用时间:31

开始使用directory.EnumerateFiles读取

读取使用时间:15

请按任意键继续……


可见,新方法的性能优势还是很明显的。除此之外,还可通过File和Directory的基于IEnumerable<T>的新方法实现一些有用的方案。请考虑以下代码:


using System;

using System.IO;

using System.Linq;

namespace ProgrammingCSharp4

{

class IOSample

{

public static void Main()

{

Console.WriteLine(“开始读取日志文件”);

//操作的目录

string dirPath=@"c:\windows\";

try

{

var errorlines=

from file in Directory.EnumerateFiles(dirPath,"*.log")

from line in File.ReadLines(file)

where line.Length>10

select string.Format("File={0},Line={1}",file,line);

File.WriteAllLines(@"C:\logfiles.log",errorlines);

}

catch(Exception)

{

}

}

}

}


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