IO输入输出
.NET标准库中,System.IO命名空间下包含了和输入/输出操作相关的工具类,包括文件系统操作、文件流、网络流等,这里我们简单介绍其中比较常用的内容。至于网络流相关的内容将在网络编程章节详细介绍。
Stream 流
所谓的Stream流,其实是一种对IO设备进行读写的抽象概念。我们读写的设备可以是磁盘、主存、网卡等,这些设备通常由硬件驱动程序来操作,而.NET为我们进一步封装了一层,将IO设备的读写操作暴露为统一接口,方便我们使用。C#中,所有的流都继承自Stream基类。
流有三个基本操作:
- Read 读操作:用于从流中读取数据,对于字节流一般都是读入一个字节数组
- Write 写操作:用于向流写入数据,对于字节流一般都是传入一个包含要写数据的字节数组
- Seek 设置读写位置:无论读写流都会自动改变一个
Position属性,每读写一个字节都会让其增加1,我们也可以手动调用Seek方法设置这个属性实现随机读写
FileStream 文件流
FileStream流用于读写文件系统中的文件,下面代码中,我们从一个文本文件中以字节流方式进行了读取,并打印了其中的内容。
// 开启文件流
FileStream fileStream = File.Open(@"E:\1.txt", FileMode.Open);
// 开辟1024字节的数组
byte[] buffer = new byte[1024];
// 从文件流中读入1024字节数据
fileStream.Read(buffer, 0, buffer.Length);
// 关闭文件流
fileStream.Close();
// 输出读取的内容
System.Text.Encoding encoding = System.Text.Encoding.UTF8;
string str = encoding.GetString(buffer);
Console.WriteLine(str);
上面代码的实现比较简单,只是尝试读取文件的前1024个字节,如果是一个较大的文件,你可能需要编写一个循环来读取其中的内容。这里还要提醒一点,流、文件句柄等资源在使用完毕后必须调用Close()或Dispose()释放,实际开发中更推荐使用控制流程语句章节介绍的using语句,它能在代码块结束或发生异常时保证资源一定被释放。
using FileStream fileStream = File.Open(@"E:\1.txt", FileMode.Open);
byte[] buffer = new byte[1024];
fileStream.Read(buffer, 0, buffer.Length);
// 离开作用域时自动释放,无需手动Close()
向文件流写入数据也十分简单,下面是一个例子。
// 开启文件流
FileStream fileStream = File.Open(@"E:/2.txt", FileMode.OpenOrCreate);
// 写入数据
string str = "你好,C#!";
System.Text.Encoding encoding = System.Text.Encoding.UTF8;
byte[] strBytes = encoding.GetBytes(str);
fileStream.Write(strBytes, 0, strBytes.Length);
// 关闭流
fileStream.Close();
代码中,我们将字符串以UTF-8编码转化为了字节数组,并写入字节流。
MemoryStream 内存流
我们进行文件和网络IO操作时,一些临时数据可以存储在内存中,以便进一步处理,MemoryStream用于以流的方式读写一块内存区域。下面例子代码简单演示了其用法。
string str = "你好,C#!";
System.Text.Encoding encoding = System.Text.Encoding.UTF8;
byte[] strBytes = encoding.GetBytes(str);
// 创建内存流
MemoryStream memoryStream = new MemoryStream(new byte[1024]);
// 向内存流写入数据
memoryStream.Write(strBytes, 0, strBytes.Length);
// 将流的读写位置重置
memoryStream.Seek(0, SeekOrigin.Begin);
// 从内存流读取数据
byte[] buf = new byte[1024];
memoryStream.Read(buf, 0, buf.Length);
Console.WriteLine(encoding.GetString(buf));
代码中我们对于同一个内存流,先写入了一部分数据,然后重置了其位置,最后又将数据重新读取了出来。
BufferedStream 缓冲流
BufferedStream需要和FileStream等基础的流类型配合使用,缓冲流使用了内存缓冲区进行块读写,相比直接使用FileStream等能够提高流读写的性能。注意这里所说的缓冲区和上面读写流使用的byte[] buf不是一个层次的概念,缓冲流中的缓冲区是.NET中更为底层的块读写实现。
// 创建缓冲流
FileStream fileStream = File.Open(@"E:\1.txt", FileMode.Open);
BufferedStream bufferedStream = new BufferedStream(fileStream);
// 读取数据
byte[] buf = new byte[1024];
bufferedStream.Read(buf, 0, buf.Length);
// 关闭流
bufferedStream.Close();
fileStream.Close();
System.Text.Encoding encoding = System.Text.Encoding.UTF8;
string str = encoding.GetString(buf);
Console.WriteLine(str);
默认情况下缓冲流使用的缓冲区为4KB,当然我们也可以手动指定其值,读写大文件时,我们可以将其调大。
NetworkStream 网络流
NetworkStream用于从网络套接字(Socket)中读写数据,有关这部分内容将在网络编程章节详细介绍。
字符流
如果读写文本文件,上面使用字节流的方式其实很不方便,我们需要以字节数组形式读取,然后将其转换为对应编码的字符串。对于文本文件.NET提供了StreamReader和StreamWriter,支持以文件行或字符数组的方式进行读取,使用字符流读写文本文件就方便很多了。
下面例子代码演示了如何使用StreamReader和StreamWriter读写文本文件。
// 创建字符读取流
StreamReader streamReader = new StreamReader(@"E:\1.txt");
// 从字符流中读取一行
string s = streamReader.ReadLine();
Console.WriteLine(s);
// 一次性读取剩余全部内容
string rest = streamReader.ReadToEnd();
// 关闭流
streamReader.Close();
// 创建字符写入流
StreamWriter streamWriter = new StreamWriter(@"E:\1.txt");
// 向流中写入数据(覆盖写入)
streamWriter.WriteLine("ha ha ha ~");
// 关闭流
streamWriter.Close();
上面例子中ReadLine()用于逐行读取,返回null表示已读到末尾;ReadToEnd()则是一次性读取剩余的全部内容。写入时StreamWriter默认覆盖文件内容,如果希望在文件末尾追加,可以使用new StreamWriter(path, append: true)构造。
文件系统操作
我们知道文件系统是操作系统的重要组成部分,文件系统提供了一系列接口用于文件和文件夹的增删改查操作,.NET也对文件系统的操作进行了便利的封装。
文件(File)和目录(Directory)
.NET中File和Directory是两个比较重要的工具类,它们被声明为静态类,其中包含了操作文件和目录的许多实用方法,包括移动、复制和删除文件(文件夹)等;此外,FileInfo和DirectoryInfo则是对于文件系统中文件和目录的抽象,这两个类则需要基于真正的文件和文件夹创建实例来使用。这两组API提供的功能其实有些重合,我们实际开发中需要根据具体使用场景具体选择。
// 创建文件
using (FileStream fs = File.Create(@"E:\3.txt"))
{
// File.Create返回一个FileStream,同样需要释放
}
// 判断文件是否存在
bool exists = File.Exists(@"E:\3.txt");
Console.WriteLine(exists);
// 删除文件
File.Delete(@"E:\2.txt");
// 移动文件
File.Move(@"E:\1.txt", @"E:\2.txt");
// 拷贝文件
File.Copy(@"E:\2.txt", @"E:\1.txt");
// 读取文件所有内容
byte[] buf = File.ReadAllBytes(@"E:\2.txt");
System.Text.Encoding encoding = System.Text.Encoding.UTF8;
string str = encoding.GetString(buf);
Console.WriteLine(str);
上面代码实现了对一些文件的操作,至于Directory其实也是类似的,它同样提供了CreateDirectory()、Exists()、Delete()、Move()等静态方法,下面是一个简单的例子。
// 创建目录(目录已存在时不会报错,也不会重复创建)
Directory.CreateDirectory(@"E:\backup");
// 判断目录是否存在
bool dirExists = Directory.Exists(@"E:\backup");
// 获取目录下所有文件的路径
string[] files = Directory.GetFiles(@"E:\backup");
// 递归获取目录下所有子目录和文件中的文件
string[] allFiles = Directory.GetFiles(@"E:\backup", "*.*", SearchOption.AllDirectories);
// 删除目录及其全部内容
Directory.Delete(@"E:\backup", recursive: true);
文件内容的简便读写
实际开发中,读写文本文件最常见的需求就是「一次性读出全部内容」或「一次性写入全部内容」,这时没必要使用流,直接使用File类提供的简便方法即可,这也是实际开发中最常用的文件读写方式。
// 一次性读取文本文件全部内容(可指定编码,默认UTF-8)
string text = File.ReadAllText(@"E:\1.txt");
// 一次性写入文本(覆盖原有内容)
File.WriteAllText(@"E:\1.txt", "你好,C#!");
// 在文件末尾追加文本
File.AppendAllText(@"E:\1.txt", "追加一行\n");
// 按行读取为字符串数组
string[] lines = File.ReadAllLines(@"E:\1.txt");
// 按行写入字符串数组
File.WriteAllLines(@"E:\1.txt", new[] { "第一行", "第二行" });
对于大文件,File.ReadLines()(注意是ReadLines而非ReadAllLines)以IEnumerable<string>的形式惰性逐行返回内容,可以配合foreach边读边处理,而不必把整个文件一次性加载进内存。
foreach (string line in File.ReadLines(@"E:\big.log"))
{
// 逐行处理,内存占用与文件大小无关
}
此外,上面介绍的FileStream、StreamReader等流类型还提供了ReadAsync()、WriteAsync()等异步版本的方法,在异步编程模型(见异步编程章节)下可以避免IO操作阻塞线程,例如File.ReadAllTextAsync()、streamReader.ReadToEndAsync()等。
Path 路径操作
Path是一个静态类,提供了对文件系统路径的解析、拼接等一系列操作。对于文件路径,相比我们自己手动操作字符串,使用Path工具类要更加靠谱。
string pathStr = @"E:\1.txt";
// 获取文件名
string filename = Path.GetFileName(pathStr);
// 获取文件名(不带扩展名)
string filenameWithoutExtension = Path.GetFileNameWithoutExtension(pathStr);
// 获取扩展名(包含点号,例如 .txt)
string extension = Path.GetExtension(pathStr);
// 获取文件的路径名
string directoryName = Path.GetDirectoryName(pathStr);
// 组合路径和文件名为完整的路径
string combinedPath = Path.Combine(@"E:\学习资料", "1.txt");
// 修改扩展名
string newPath = Path.ChangeExtension(pathStr, ".bak");
Path类中的方法都是纯字符串处理,不会去访问真实的文件系统,因此即使路径对应的文件不存在也可以正常使用。另外,不同操作系统的路径分隔符不同(Windows为\,Linux为/),这也是拼接路径应当使用Path.Combine()而非手动拼接字符串的原因之一。