.NET Framework将文件视为数据流。流是一系列用字节表示的数据分组。数据流有底层存储介质,这些存储介质通常称为支撑存储空间(backing store),它们为流提供了源。所幸的是,.NET Framework提供了File、Directory和Path类,这使得处理文件和目录更容易。
命名空间 System.IO 包含处理缓冲流和非缓冲流所需的所有类。缓冲流让操作系统创建内部缓冲区,并使用它以效率最高的增量读写数据。
本章介绍如何处理文件;使用File、Directory和Path类来查看和管理文件系统以及读写文件;还将介绍如何使用Stream类及其派生类来执行复杂的读写操作。
一、文件和目录
可将文件视为字节序列,它有明确的名称和固定的支撑存储介质。要操作文件,需要使用路径、磁盘存储空间、文件名和目录名。.NET Framework在命名空间System.IO中提供了多个类,能够轻松地处理文件。
1.1 使用路径
路径是一个字符串,指出了文件或目录的位置,可包含绝对位置信息,也可包含相对位置信息。绝对路径指定了完整的位置,而相对路径只指定了部分位置。使用相对路径查找指定的文件时,将以当前位置为起点。
ps:当前位置
每个进程都有进程级当前位置,这通常是加载进程的位置,但并非总是如此。
Path类提供了一些静态方法,让您能够以跨平台的方式处理路径字符串。虽然Path类的大多数成员都不与文件系统交互,但是验证指定路径字符串是否包含有效的字符。
下表列出了Path类中常用的方法:
方法 | 描述 |
---|---|
ChangeExtension | 修改扩展名 |
Combine | 将多个字符串合并成路径 |
GetDirectoryName | 获取指定路径中的目录 |
GetExtension | 获取指定路径中的扩展名 |
GetFileName | 获取指定路径中的文件名和扩展名 |
GetFlieNameWithoutExtension | 获取指定路径中的文件名,但不包含扩展名 |
GetPathRoot | 获取指定路径的根目录 |
GerRandomFileName | 获取一个随机名称 |
GetTempFileName | 穿件一个随机命名且唯一的临时文件,并返回该文件的完整路径 |
GetTempPath | 获取指向临时文件夹的路径 |
1.2 特殊目录
Windows操作系统包含很多经常被应用程序使用的特殊文件夹。通常,这些文件夹是由操作系统指定的,但安装Windows操作系统时,用户也可显式地指定它们。因此,这些文件夹的名称和位置可能随计算机而异。
要确定这些特殊文件夹(如 Windows 文件夹)的路径,最合适的方式是使用方法Environment.GetFolderPath。这个方法接受一个Environment.SpecialFolder枚举值作为参数,该参数指出了要获取哪个特殊文件夹的路径。
下表列出了一些常用的Environment.SpecialFolder枚举值
枚举值 | 描述 |
---|---|
ApplicationData | 用于存储当前漫游用户使用的应用程序数据的目录 |
CommonApplicationData | 用于存储所有用户共用的应用程序数据的目录 |
LocalApplicationData | 用于存储当前非漫游用户使用的应用程序数据的目录 |
CommonDocuments | 包含所有用户共用文档的文件系统目录 |
Desktop | 逻辑桌面,而不是物理文件系统位置 |
DesktopDirectory | 实际存储桌面文件对象的目录,不要将其与虚拟文件夹Desktop混为一谈 |
MyDocuments | “我的文档”文件夹,与Personal等价 |
Personal | 用作文档仓库的目录 |
System | System目录 |
Windows | Windows或SYSROOT目录,对应于环境变量%windir%或%SYSTEMROOT% |
1.3 DirectoryInfo和FileInfo类
DirectoryInfo和FileInfo类都是从FileSystemInfo派生而来的,而FileSystemInfo类可表示文件或目录,包含用于操作文件和目录的方法。实例化 FileSystemInfo 类时,将缓存目录或文件信息,因此必须使用Refresh方法进行刷新,以确保信息是最新的。
DirectoryInfo 的实例成员提供了很多属性和执行常见操作(如复制、移动、创建和列举目录)的方法。
下表列出了DirectoryInfo类的常用方法和属性:
成员 | 描述 |
---|---|
Create | 创建目录 |
CreateSubdirectory | 在指定路径中创建子目录 |
Delete | 删除当前目录,并可指定是否要删除其中的所有文件和子目录 |
EnumerateDirectories | 获取一个可枚举集合,其中包含当前目录中的目录信息 |
EnumerateFiles | 获取一个可枚举集合,其中包含当前目录中的文件信息 |
EnumerateFileSystemInfos | 获取一个可枚举集合,其中包含当前目录中的文件和目录信息 |
Exists | 指出磁盘中是否存在当前目录 |
FullName | 获取当前目录的完整路径 |
MoveTo | 将当前目录(包括其中的所有文件和子目录)移到指定的位置 |
Name | 获取当前目录的名称 |
Parent | 获取当前目录的父目录 |
Refresh | 刷新缓存的目录信息 |
Root | 获取路径的根目录部分 |
如下程序演示了如何使用DirectoryInfo类执行一些常见的操作:
public class DirectoryInfoExample
{
public static void Main()
{
string tempPath = Path.GetTempFileName();
DirectoryInfo directoryInfo = new DirectoryInfo(tempPath);
try
{
if (directoryInfo.Exists)
{
Console.WriteLine("The directory already exists.");
}
else
{
directoryInfo.Create();
Console.WriteLine("The directory was successfully created.");
directoryInfo.Delete();
Console.WriteLine("The directory was deleted.");
}
}
catch (IOException e)
{
Console.WriteLine("An error occurred:{0}", e.Message);
}
}
}
FileInfo类包含的实例成员提供了大量属性和执行常见文件操作(如复制、移动、创建和打开文件)的方法。
下表列出了常用的方法和属性:
成员 | 描述 |
---|---|
AppendText | 创建一个StreamWriter,用于在当前文件末尾追加文本 |
Attributes | 获取或设置当前文件的特性 |
CopyTo | 将当前文件复制到新文件 |
Create | 创建文件 |
CreateText | 创建或打开一个文件,以便向其中写入文本 |
Delete | 删除当前文件 |
Directory | 获取父目录 |
DirectoryName | 获取父目录的名称 |
Exists | 判断磁盘中是否存在当前文件 |
Extension | 当前文件的扩展名 |
FullName | 获取当前文件的完整路径 |
IsReadOnly | 获取或设置一个值,这个值决定了当前文件是否是只读的 |
Length | 当前文件的长度 |
MoveTo | 将当前文件移到指定的位置 |
Name | 获取当前文件的名称 |
Open | 打开一个文件 |
OpenRead | 打开一个现有的文件以便读取 |
OpenText | 打开一个现有的文本文件以便读取 |
OpenWrite | 打开一个现有的文件以便写入 |
Refresh | 刷新缓存的文件信息 |
Replace | 使用当前文件的内容替换指定文件的内容 |
如下程序演示了如何使用FileInfo类执行一些常见的操作:
public class FileInfoExample
{
public static void Main()
{
string tempFile = Path.GetTempFileName();
FileInfo fileInfo = new FileInfo(tempFile);
try
{
if (!fileInfo.Exists)
{
using (StreamWriter writer = fileInfo.CreateText())
{
writer.WriteLine("Line 1");
writer.WriteLine("Line 2");
}
}
fileInfo.CopyTo(Path.GetTempFileName());
fileInfo.Delete();
}
catch (IOException e)
{
Console.WriteLine("An error occurred: {0}", e.Message);
}
}
}
ps:流是可释放的
使用完流后,务必调用Close方法释放其占用的资源。也可将流放在一条using语句中,这是一种更好的方法,可确保流会被正确地关闭。
1.4 Directory和File类
如果不想创建DirectoryInfo和FileInfo类的实例,那么可以使用Directory和File类。这些类只提供了静态方法,用于执行DirectoryInfo和FileInfo支持的目录和文件操作。
下表列出了Directory类的常用方法:
成员 | 描述 |
---|---|
CreateDirectory | 创建指定路径中的所有目录 |
Delete | 删除指定的目录 |
EnumerateDirectories | 获取一个可枚举集合,其中包含指定路径中的目录名 |
EnumerateFiles | 获取一个可枚举集合,其中包含指定路径中的文件名 |
EnumerateFileSystemEntries | 获取一个可枚举集合,其中包好指定路径中所有文件和子目录的名称 |
Exists | 指出指定的路径是否存在于磁盘中 |
GetCurrentDirectory | 获取当前工作目录 |
GetDirectoryRoot | 获取当前路径的卷信息、根信息或两者 |
GetLogicalDrives | 获取当前计算机中的逻辑驱动器的名称 |
GetParent | 获取指定路径的父目录 |
Move | 将文件或目录(包括其中的所有文件和子目录)移到指定位置 |
如下程序执行的一些操作,但使用的是Directory类,而不是DirectoryInfo类:
public class DirectoryInfoExample
{
public static void Main()
{
string tempPath = Path.GetTempFileName();
try
{
if (DirectoryInfo.Exists(tempPath))
{
Console.WriteLine("The directory already exists.");
}
else
{
DirectoryInfo.CreateDircectory(path);
Console.WriteLine("The directory was successfully created.");
DirectoryInfo.Delete(path);
Console.WriteLine("The directory was deleted.");
}
}
catch (IOException e)
{
Console.WriteLine("An error occurred:{0}", e.Message);
}
}
}
Directory 和 DirectoryInfo 类之间的一个重要差别体现在方法 EnumerateFiles、EnumerateDirectories和EnumerateFileSystemEntries。在Directory类中,这些方法返回一个包含目录和文件名的 IEnumerable<string>,而在 DirectoryInfo 类中,这些方法分别返回IEnumerable<FileInfo>、IEnumerable<DirectoryInfo>和IEnumerable<FileSystemInfo>。
下表列出了File类的常用方法
成员 | 描述 |
---|---|
AppendAllLines | 在文件末尾追加文本行,然后关闭文件 |
AppendAllText | 将指定的字符串追加到文件末尾,如果文件不存在,就创建它 |
AppendText | 创建一个StreamWriter,用于将文本追加到当前文件末尾 |
Copy | 将现有文件复制到新文件 |
Create | 创建一个文件 |
CreateText | 创建或打开一个文件,以便写入文本 |
Delete | 删除指定的文件 |
Exists | 确定指定的文件是否存在于磁盘中 |
GetAttributes | 获取指定文件的特性 |
Move | 将指定文件移到新位置 |
OpenRead | 打开一个现有的文件以便读取 |
OpenText | 打开一个现有的文本文件以便读取 |
OpenWrite | 打开一个现有的文件以便写入 |
ReadAllBytes | 打开一个二进制文件,将其内容读取到一个字节数组中,然后关闭该文件 |
ReadAllLines | 打开一个文本文件,将所有行都读取到一个字符串中,然后关闭该文件 |
ReadAllText | 打开一个文本文件,将所有行都读取到一个字符串中,然后关闭该文件 |
ReadLines | 读取文件中的行 |
Replace | 使用其他文件的内容替换指定文件的内容 |
SetAttributes | 设置指定文件的特性 |
WriteAllBytes | 创建一个新文件,将指定字节写入其中,然后关闭该文件 |
WriteAllLines | 创建一个新的文本文件,将一个或多个字符串写入其中,然后关闭该文件 |
WriteAllText | 创建一个新文件,将指定字符串写入其中,然后关闭该文件 |
以下示例使用了使用File类
public class FileExample
{
public static void Main()
{
string tempFile = Path.GetTempFileName();
try
{
if (!File.Exists(tempFile))
{
using (StreamWriter writer = File.CreateText(tempFile))
{
writer.WriteLine("Line 1");
writer.WrtieLine("Line 2");
}
}
File.Copy(tempFile, Path.GetTempFileName());
File.Delete(tempFile);
}
catch (IOException e)
{
Console.WriteLine("An error occurred: {0}", e.Message);
}
}
}
二、读写数据
要处理文件中的数据,无论是读取还是写入,都可以使用 Stream 类表示的流。.NET Framework中所有基于类的流都是从这个类派生而来的。
下表列出了Stream类的常用成员:
成员 | 描述 |
---|---|
CanRead | 之处当前流是否支持读取 |
CanWrite | 之处当前流是否支持写入 |
Close | 关闭当前流 |
CopyTo | 将当前流的内容复制到另一个流中 |
Flush | 清空所有缓冲区,并将缓冲区的数据都写入支持存储介质 |
Read | 从当前流中读取字节序列 |
Write | 将一系列字节写入当前流中 |
2.1 二进制文件
不确定文件内容的类型时,通常最好将其视为二进制文件,这种文件不过是一个字节流。要从二进制文件中读取数据,可使用File类的静态方法OpenRead,它返回一个FileStream:
FileStream input = File.OpenRead(Path.GetTempFileName());
然后,可对该FileStream调用Read方法,将数据读取到指定的缓冲区中。缓冲区(buffer)是一个字节数组,可用于存储 Read 方法返回的数据。传递缓冲区、要读取的字节数以及数据存储位置相对缓冲区开头的偏移量后,Read方法将从支持存储区读取指定数量的字节,将其存储到缓冲区,并返回实际读取的字节数:
byte[] buffer = new byte[1024];
int bytesRead = input.Read(buffer, 0, 1024);
当然,从流中读取数据并非能执行的唯一操作。将二进制数据写入流也是一种常见的操作,完成这种操作的方式与读取数据类似。首先使用File类的OpenWrite方法打开一个二进制文件,以便写入,然后对返回的FileStream调用方法Write,将缓冲区中的数据写入支持存储区。调用 Write 方法时,需要传递如下参数:包含要写入的数据的缓冲区、从离缓冲区开头多远的地方开始读取以及写入多少个字节,如下所示。
FileStream output = File.OpenWrite(Path.GetTempFileName());
output.Write(buffer, 0, bytesRead);
如下代码是一个完整的示例,它从一个二进制文件中读取数据,并将数据写入另一个二进制文件。这个示例不断地读写字节,直到Read方法返回0,这表明再没有字节可供读取了。
public class BinaryReaderWriter
{
const int BufferSize = 1024;
public static void Main()
{
string tempPath = Path.GetTempFileName();
string tempPath2 = Path.GetTempFileName();
if (File.Exsists(tempPath))
{
using (FileStream input = FileExample.OpenRead(tempPath))
{
byte[] buffer = new byte[BufferSize];
int bytesRead;
using (FileStream output = File.OpenWrite(tempPath2))
{
while ((bytesRead = input.Read(buffer, 0, BufferSize)) > 0)
{
output.Write(buffer, 0, bytesRead);
}
}
}
}
}
}
2.2 缓冲流
在前一个示例中使用FileStream时,需要指定用于读取数据的缓冲区以及该缓冲区的大小。在很多情况下,如果让操作系统判断要读取多少个字节,效率可能更高。
BufferedStream 让操作系统创建内部缓冲区,并以它认为效率最高的增量填充缓冲区。它仍以您提供的增量对您提供的缓冲区进行填充,但填充时使用内部缓冲区的数据,而不直接使用支持存储区中的数据。要创建缓冲流,可使用Stream创建一个新的BufferedStream实例,如下所示:
public class BinaryReaderWriter
{
const int BufferSize = 1024;
public static void Main()
{
string tempPath = Path.GetTempFileName();
string tempPath2 = Path.GetTempFileName();
if (File.Exsists(tempPath))
{
using (BufferedStream input = new BufferedStream(File.OpenRead(tempPath)))
{
byte[] buffer = new byte[BufferSize];
int bytesRead;
using (BufferedStream output = new BufferedStream(File.OpenWrite(tempPath2)))
{
while ((bytesRead = input.Read(buffer, 0, Buffersize)) > 0)
{
output.Write(buffer, 0, bytesRead);
}
}
}
}
}
}
2.3 文本文件
Stream实例不仅可指向二进制文件,还可指向文本文件,并可使用其Read和Write方法来读写数据。读写字节数组,而不是字符串,这样比较不方便。为简化文本文件的处理工作,.NET Framework提供了StreamReader和StreamWriter类。
StreamReader提供了一个Read方法,它每次读取一个字符;还提供了一个ReadLine方法,它每次读取一行字符,并返回一个字符串。行是一系列字符,以换行符(\n)、回车(\r)或换行和回车(\r\n)结尾。到达输入流末尾后,ReadLine将返回null;否则返回一行字符,但不包含行尾字符。要写入文本数据,可使用StreamWriter类的WriteLine方法。
如下是一个读写文本数据的示例:
public class TextReaderWriter
{
public static void Main()
{
string tempPath = Path.GetTempFileName();
string tempPath2 = Path.GetTempFileName();
if (File.Exsists(tempPath))
{
using (StreamReader reader = new File.OpenText(tempPath))
{
string buffer = null;
using (StreamWriter writer = new StreamWriter(tempPath2))
{
while ((buffer = reader.ReadLine()) !=null)
{
writer.WriteLine(buffer);
}
}
}
}
}
}
2.4 使用File类读写数据
鉴于从文件(无论是文本还是二进制文件)读写数据是一种常见任务,因此File类提供了多个方法,使得这项任务比直接使用流更方便。
要读写二进制数据,可分别使用方法ReadAllBytes和WriteAllBytes。这些方法打开文件,读取或写入字节,然后关闭文件。
如下程序使用的是方法ReadAllBytes和WriteAllBytes:
public class TextReaderWriter
{
public static void Main()
{
string tempPath = Path.GetTempFileName();
string tempPath2 = Path.GetTempFileName();
if (File.Exsists(tempPath))
{
byte[] data = File.ReadAllBytes(tempPath);
File.WriteAllBytes(tempPath2, data);
}
}
}
读写文本数据也很容易。要读取文本数据,可使用方法ReadAllLines或ReadAllText;要写入文本数据,可使用方法WriteAllLines或WriteAllText。方法ReadAllLines将文件中的所有行都读入一个字符串数组中,其中每一行都是该数组的一个元素;而 ReadAllText 将所有行都读入一个字符串中。
方法WriteAllLines将字符串数组的每个元素都写入文件,而WriteAllText将一个字符串的内容写入文件。如果指定的文件存在,那么这两个方法都覆盖它;否则,创建一个新文件。要在现有文件末尾追加文本,可使用方法 AppendAllLines 或 AppendAllText;如果需要打开一个流,就可使用方法AppendText。
如下程序使用方法ReadAllLines和WriteAllLines
public class TextReaderWriterFile
{
public static void Main()
{
string tempPath = Path.GetTempFileName();
string tempPath2 = Path.GetTempFileName();
if (File.Exsists(tempPath))
{
string[] data = File.ReadAllLines(tempPath);
File.WriteAllLines(tempPath2, data);
}
}
}
使用方法ReadAllLines或ReadAllText的缺点是,首先必须将整个文件读入内存。为解决这个问题,可使用方法ReadLines,它返回一个IEnumerable<string>集合。由于这个方法返回一个 IEnumerable<string>集合,因此可在返回整个集合前就开始枚举它。
如下程序使用了WriteAllLines和ReadLines:
public class TextReaderWriterFile
{
public static void Main()
{
string tempPath = Path.GetTempFileName();
string tempPath2 = Path.GetTempFileName();
if (File.Exsists(tempPath))
{
File.WriteAllLines(tempPath, File.ReadLines(tempPath2));
}
}
}