第 29 节 · 第四部分 进阶特性

文件的输入与输出

打开文件得到一个流,流从“当前位置”一个字节一个字节地读写;打开时用 FileMode、FileAccess、FileShare 说清怎么开、能读还是能写、让不让别人同时打开。

时长 8:51授课计划第 11 周

学完这一节,你能

讲义

跟着视频的顺序写。每行代码都有注释,代码和运行结果都是实际运行得到的。

1文件和流

文件是存在磁盘上的一组数据,有自己的名字和所在的目录路径。程序打开文件来读或写时,它就变成了一个流:一串排着队传过去的字节。从文件读进程序用输入流,从程序写进文件用输出流。

视频里用字节纸带机演示:长槽是文件,每格放一个字节;槽上的小车是流,停在哪一格,下一次就从哪一格读写,这个格号叫位置(Position)。这只是示意,真实文件里没有格子,字节是一个挨一个连着存的。

2System.IO 里的类

管文件的类都在 System.IO 命名空间里。教程列了十五个常用的类,按用途分两组:

  • 搬运数据的流:FileStream 读写文件里的字节;MemoryStream 读写内存里的数据;BufferedStream 给字节流加一块临时缓冲(先攒在内存里,再一次性读写);StreamReader、StreamWriter 读写文本;BinaryReader、BinaryWriter 按二进制格式读写原始数据;StringReader、StringWriter 读写内存里的字符串。
  • 管文件和文件夹本身:File、FileInfo 处理文件(创建、复制、删除);Directory、DirectoryInfo 管文件夹;DriveInfo 查驱动器信息;Path 处理路径。

其中 File、Directory、Path 是静态类,直接用类名调用方法,不用 new;其余的都要先 new 出对象再用。

3FileStream

FileStream 负责文件的读、写和关闭,派生自抽象类 Stream。新建或打开文件,都要先创建一个 FileStream 对象,给四样东西:文件名、打开模式 FileMode、访问方式 FileAccess、共享方式 FileShare。后三样都是枚举,要填的是枚举里的一个值。

C# · 第 9-10 行
9FileStream F = new FileStream("sample.txt", FileMode.Open, FileAccess.Read, FileShare.Read);// 打开已有的 sample.txt 来读,也允许别人同时读
10Console.WriteLine("能读 " + F.CanRead + " 能写 " + F.CanWrite);// CanRead、CanWrite 告诉你这个流能不能读、能不能写
运行结果
能读 True   能写 False

第四个参数可以不写,不写默认是 FileShare.Read。对应到纸带机上,三个枚举就是小车上的模式旋钮、读写灯和槽口的闸门。

4FileMode:怎么打开

六种模式,关键看两件事:文件已经有了怎么办,没有又怎么办。

  • Append:打开已有文件,位置放到末尾接着写;没有就新建。
  • Create:总是得到一个空文件,已有的会被清空重写。
  • CreateNew:只管新建,文件已存在就抛出异常。
  • Open:只打开已有的文件。
  • OpenOrCreate:有就打开,没有就新建。
  • Truncate:打开已有文件,截成 0 字节,但保留创建日期。

拿一个 3 字节的旧文件和一个不存在的文件,六种模式各试一遍。Try 是程序里的辅助方法:用模式 m 打开文件,返回打开后的长度和位置,出错就返回异常名。循环变量 m 依次取 FileMode 的六个值。

C# · 第 15-17 行
15File.WriteAllBytes("old.dat", new byte[] { 7, 8, 9 });// 每轮先准备一个 3 字节的旧文件
16File.Delete("new.dat");// 再确保 new.dat 不存在
17Console.WriteLine(m.ToString().PadRight(15) + Try("old.dat", m).PadRight(21) + Try("new.dat", m));// 两个文件各试一次;PadRight 在右边补空格对齐
C# · 第 25-26 行
25FileStream F = new FileStream(name, m, FileAccess.Write);// 用模式 m 打开,只写
26string s = "长度 " + F.Length + ",位置 " + F.Position;// 记下打开后的长度和位置
运行结果
模式           已有文件(3 字节)   没有文件
Append         长度 3,位置 3            长度 0,位置 0
Create         长度 0,位置 0            长度 0,位置 0
CreateNew      IOException          长度 0,位置 0
Open           长度 3,位置 0            FileNotFoundException
OpenOrCreate   长度 3,位置 0            长度 0,位置 0
Truncate       长度 0,位置 0            FileNotFoundException

Open、Truncate 遇到不存在的文件,抛出 FileNotFoundException;CreateNew 遇到已有文件,抛出 IOException。Create 遇到已有文件会把它清空,创建时间不变。Thread.Sleep 让程序暂停一会儿(线程是程序里的一条执行路径,最后一节讲):

C# · 第 12-19 行
12DateTime before = File.GetCreationTime("old.dat");// 记下旧文件的创建时间
13Thread.Sleep(1500);// 暂停 1.5 秒,让时间拉开差距
14
15FileStream F = new FileStream("old.dat", FileMode.Create);// 用 Create 打开旧文件
16F.Close();// 关上
17
18Console.WriteLine("长度:" + new FileInfo("old.dat").Length);// new FileInfo(...).Length 取文件长度:变成 0
19Console.WriteLine("创建时间没变:" + (File.GetCreationTime("old.dat") == before));// 和原来的创建时间比
运行结果
长度:0
创建时间没变:True

还要注意,Append 只能配合写入,配上只读会抛出参数异常:

C# · 第 13 行
13FileStream F = new FileStream("old.dat", FileMode.Append, FileAccess.Read);// Append 配只读
运行结果
Append + Read:ArgumentException

5FileAccess:能读还是能写

只有三个成员:Read 只读,Write 只写,ReadWrite 又能读又能写,就是小车上的读灯和写灯。循环变量 a 依次取 FileAccess 的三个值。

C# · 第 14-15 行
14FileStream F = new FileStream("test.dat", FileMode.Open, a);// 依次用 Read、Write、ReadWrite 打开
15Console.WriteLine(a.ToString().PadRight(10) + "能读 " + F.CanRead + " 能写 " + F.CanWrite);// 打印能不能读、能不能写
C# · 第 20-21 行
20FileStream R = new FileStream("test.dat", FileMode.Open, FileAccess.Read);// 只读打开
21R.WriteByte(4);// 硬要写进一个字节
运行结果
Read      能读 True   能写 False
Write     能读 False   能写 True
ReadWrite 能读 True   能写 True
只读却写入:NotSupportedException

6FileShare:让不让别人同时打开

  • None:谢绝共享,文件关上之前,谁再来打开都会失败。
  • Read、Write、ReadWrite:允许别人再打开来读、来写、读写都行。
  • Delete:允许别人删除这个文件。
  • Inheritable:让子进程(本程序启动的另一个程序)继承文件句柄(打开文件后拿到的编号),Windows 的底层接口不直接支持它。

就算允许共享,别人可能还需要额外的权限才能真正访问。这几个值还能用 | 组合,比如 FileShare.Read | FileShare.Write。下面的循环变量 s 依次取 FileShare.None 和 FileShare.Read。

C# · 第 14-17 行
14FileStream A = new FileStream("test.dat", FileMode.Open, FileAccess.Read, s);// A 先打开,共享方式依次是 None、Read
15try// B 再来打开可能失败,放进 try
16{// try 开始
17 FileStream B = new FileStream("test.dat", FileMode.Open, FileAccess.Read, FileShare.Read);// B 只是想读
运行结果
A 用 None,B 再打开来读:IOException
A 用 Read,B 再打开来读:成功

A 用 None 时,就算 A 和 B 在同一个程序里,B 也被挡住。

7完整例子:写 20 个字节再读出来

C# · 第 10-16 行
10FileStream F = new FileStream("test.dat",// 打开 test.dat(相对路径:在当前运行的文件夹里找)
11FileMode.OpenOrCreate, FileAccess.ReadWrite);// 有就打开、没有就新建;能读也能写
12
13for (int i = 1; i <= 20; i++)// i 从 1 到 20
14{// 写入循环开始
15 F.WriteByte((byte)i);// 每个数占一个字节写进去,位置往后挪一格
16}// 写入循环结束:位置停在第 20 格后面
C# · 第 18-24 行
18F.Position = 0;// 位置设回 0,回到开头
19
20for (int i = 0; i <= 20; i++)// i 从 0 到 20:一共读 21 次,可文件里只有 20 个字节
21{// 读取循环开始
22 Console.Write(F.ReadByte() + " ");// 读一个字节,位置往后挪一格;读到末尾返回 -1
23}// 读取循环结束
24F.Close();// 把文件关上
查看完整代码(28 行)
C#
1using System;// 引入 System 命名空间
2using System.IO;// 引入 System.IO,文件相关的类都在这里
3
4namespace FileIOApplication// 定义命名空间
5{// 命名空间开始
6 class Program// 定义 Program 类
7 {// Program 类开始
8 static void Main(string[] args)// 程序入口 Main 方法
9 {// Main 开始
10 FileStream F = new FileStream("test.dat",// 创建 FileStream,文件名 test.dat
11 FileMode.OpenOrCreate, FileAccess.ReadWrite);// 有就打开、没有就新建;能读能写
12
13 for (int i = 1; i <= 20; i++)// 写 20 次
14 {// 写入循环开始
15 F.WriteByte((byte)i);// (byte)i 把 int 转成一个字节再写入
16 }// 写入循环结束
17
18 F.Position = 0;// 回到开头
19
20 for (int i = 0; i <= 20; i++)// 读 21 次
21 {// 读取循环开始
22 Console.Write(F.ReadByte() + " ");// 打印读出的字节和一个空格
23 }// 读取循环结束
24 F.Close();// 关闭文件
25 Console.ReadKey();// 等待按键(现在的 VS 不写也会停住)
26 }// Main 结束
27 }// Program 类结束
28}// 命名空间结束
运行结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 -1

最后那次没有字节可读,ReadByte 返回 -1。生成的 test.dat 正好 20 个字节。上一节说过,把 Close 放进 finally,就算中途出了异常也一定会关上。

8进一步的内容

教程把进一步的内容分成三个主题:

  1. 文本文件的读写,用 StreamReader 和 StreamWriter。
  2. 二进制文件的读写,用 BinaryReader 和 BinaryWriter。
  3. 文件系统的操作:浏览、定位文件和文件夹。这些类在 Windows、macOS 和 Linux 上都能用。

文本读写的例子:先写两行、关上,再把整个文件读出来。

C# · 第 10-17 行
10StreamWriter sw = new StreamWriter("note.txt");// 创建写入器,打开 note.txt 来写
11sw.WriteLine("第一行");// 写一行
12sw.WriteLine("第二行");// 再写一行
13sw.Close();// 关上,内容才真正存进文件
14
15StreamReader sr = new StreamReader("note.txt");// 创建读取器,打开 note.txt 来读
16Console.Write(sr.ReadToEnd());// ReadToEnd 把整个文件读成一个字符串
17sr.Close();// 关上
运行结果
第一行
第二行

9小结

  • 文件打开来读写时就成了流,流是一串字节。
  • System.IO 里的类,一组搬运数据,一组管文件和文件夹。
  • 创建 FileStream 要给文件名、FileMode、FileAccess 和 FileShare。
  • 读写都从当前位置开始;读到末尾,ReadByte 返回 -1。

下一节:想给代码贴一张说明标签,比如告诉编译器“这个方法已经过时了,别再用”,该怎么写?

检查题

5 道,选完马上告诉你对不对。

  1. 1教程实例第二个循环 for (int i = 0; i <= 20; i++) 读 test.dat(20 个字节),最后一次 ReadByte 得到什么?

  2. 2文件不存在时,哪种模式会抛出 FileNotFoundException?

  3. 3new FileStream("test.dat", FileMode.Open, FileAccess.Read) 之后调用 WriteByte(4),结果?

  4. 4A 用 FileShare.None 打开文件,同一个程序里 B 再打开来读,结果?

  5. 5下面哪个类不用 new,直接用类名调用方法?