1文件和流
文件是存在磁盘上的一组数据,有自己的名字和所在的目录路径。程序打开文件来读或写时,它就变成了一个流:一串排着队传过去的字节。从文件读进程序用输入流,从程序写进文件用输出流。
视频里用字节纸带机演示:长槽是文件,每格放一个字节;槽上的小车是流,停在哪一格,下一次就从哪一格读写,这个格号叫位置(Position)。这只是示意,真实文件里没有格子,字节是一个挨一个连着存的。
2System.IO 里的类
管文件的类都在 System. 命名空间里。教程列了十五个常用的类,按用途分两组:
- 搬运数据的流:
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。后三样都是枚举,要填的是枚举里的一个值。
| 9 | FileStream F = new FileStream("sample.txt", FileMode.Open, FileAccess.Read, FileShare.Read); | // 打开已有的 sample.txt 来读,也允许别人同时读 |
| 10 | Console.WriteLine("能读 " + F.CanRead + " 能写 " + F.CanWrite); | // CanRead、CanWrite 告诉你这个流能不能读、能不能写 |
能读 True 能写 False
第四个参数可以不写,不写默认是 FileShare.。对应到纸带机上,三个枚举就是小车上的模式旋钮、读写灯和槽口的闸门。
4FileMode:怎么打开
六种模式,关键看两件事:文件已经有了怎么办,没有又怎么办。
Append:打开已有文件,位置放到末尾接着写;没有就新建。Create:总是得到一个空文件,已有的会被清空重写。CreateNew:只管新建,文件已存在就抛出异常。Open:只打开已有的文件。OpenOrCreate:有就打开,没有就新建。Truncate:打开已有文件,截成 0 字节,但保留创建日期。
拿一个 3 字节的旧文件和一个不存在的文件,六种模式各试一遍。Try 是程序里的辅助方法:用模式 m 打开文件,返回打开后的长度和位置,出错就返回异常名。循环变量 m 依次取 FileMode 的六个值。
| 15 | File.WriteAllBytes("old.dat", new byte[] { 7, 8, 9 }); | // 每轮先准备一个 3 字节的旧文件 |
| 16 | File.Delete("new.dat"); | // 再确保 new.dat 不存在 |
| 17 | Console.WriteLine(m.ToString().PadRight(15) + Try("old.dat", m).PadRight(21) + Try("new.dat", m)); | // 两个文件各试一次;PadRight 在右边补空格对齐 |
| 25 | FileStream F = new FileStream(name, m, FileAccess.Write); | // 用模式 m 打开,只写 |
| 26 | string 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. 让程序暂停一会儿(线程是程序里的一条执行路径,最后一节讲):
| 12 | DateTime before = File.GetCreationTime("old.dat"); | // 记下旧文件的创建时间 |
| 13 | Thread.Sleep(1500); | // 暂停 1.5 秒,让时间拉开差距 |
| 14 | | |
| 15 | FileStream F = new FileStream("old.dat", FileMode.Create); | // 用 Create 打开旧文件 |
| 16 | F.Close(); | // 关上 |
| 17 | | |
| 18 | Console.WriteLine("长度:" + new FileInfo("old.dat").Length); | // new FileInfo(...).Length 取文件长度:变成 0 |
| 19 | Console.WriteLine("创建时间没变:" + (File.GetCreationTime("old.dat") == before)); | // 和原来的创建时间比 |
长度:0 创建时间没变:True
还要注意,Append 只能配合写入,配上只读会抛出参数异常:
| 13 | FileStream F = new FileStream("old.dat", FileMode.Append, FileAccess.Read); | // Append 配只读 |
Append + Read:ArgumentException
5FileAccess:能读还是能写
只有三个成员:Read 只读,Write 只写,ReadWrite 又能读又能写,就是小车上的读灯和写灯。循环变量 a 依次取 FileAccess 的三个值。
| 14 | FileStream F = new FileStream("test.dat", FileMode.Open, a); | // 依次用 Read、Write、ReadWrite 打开 |
| 15 | Console.WriteLine(a.ToString().PadRight(10) + "能读 " + F.CanRead + " 能写 " + F.CanWrite); | // 打印能不能读、能不能写 |
| 20 | FileStream R = new FileStream("test.dat", FileMode.Open, FileAccess.Read); | // 只读打开 |
| 21 | R.WriteByte(4); | // 硬要写进一个字节 |
Read 能读 True 能写 False Write 能读 False 能写 True ReadWrite 能读 True 能写 True 只读却写入:NotSupportedException
6FileShare:让不让别人同时打开
None:谢绝共享,文件关上之前,谁再来打开都会失败。Read、Write、ReadWrite:允许别人再打开来读、来写、读写都行。Delete:允许别人删除这个文件。Inheritable:让子进程(本程序启动的另一个程序)继承文件句柄(打开文件后拿到的编号),Windows 的底层接口不直接支持它。
就算允许共享,别人可能还需要额外的权限才能真正访问。这几个值还能用 | 组合,比如 FileShare.。下面的循环变量 s 依次取 FileShare. 和 FileShare.。
| 14 | FileStream A = new FileStream("test.dat", FileMode.Open, FileAccess.Read, s); | // A 先打开,共享方式依次是 None、Read |
| 15 | try | // 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 个字节再读出来
| 10 | FileStream F = new FileStream("test.dat", | // 打开 test.dat(相对路径:在当前运行的文件夹里找) |
| 11 | FileMode.OpenOrCreate, FileAccess.ReadWrite); | // 有就打开、没有就新建;能读也能写 |
| 12 | | |
| 13 | for (int i = 1; i <= 20; i++) | // i 从 1 到 20 |
| 14 | { | // 写入循环开始 |
| 15 | F.WriteByte((byte)i); | // 每个数占一个字节写进去,位置往后挪一格 |
| 16 | } | // 写入循环结束:位置停在第 20 格后面 |
| 18 | F.Position = 0; | // 位置设回 0,回到开头 |
| 19 | | |
| 20 | for (int i = 0; i <= 20; i++) | // i 从 0 到 20:一共读 21 次,可文件里只有 20 个字节 |
| 21 | { | // 读取循环开始 |
| 22 | Console.Write(F.ReadByte() + " "); | // 读一个字节,位置往后挪一格;读到末尾返回 -1 |
| 23 | } | // 读取循环结束 |
| 24 | F.Close(); | // 把文件关上 |
查看完整代码(28 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | using System.IO; | // 引入 System.IO,文件相关的类都在这里 |
| 3 | | |
| 4 | namespace 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进一步的内容
教程把进一步的内容分成三个主题:
- 文本文件的读写,用
StreamReader和StreamWriter。 - 二进制文件的读写,用
BinaryReader和BinaryWriter。 - 文件系统的操作:浏览、定位文件和文件夹。这些类在 Windows、macOS 和 Linux 上都能用。
文本读写的例子:先写两行、关上,再把整个文件读出来。
| 10 | StreamWriter sw = new StreamWriter("note.txt"); | // 创建写入器,打开 note.txt 来写 |
| 11 | sw.WriteLine("第一行"); | // 写一行 |
| 12 | sw.WriteLine("第二行"); | // 再写一行 |
| 13 | sw.Close(); | // 关上,内容才真正存进文件 |
| 14 | | |
| 15 | StreamReader sr = new StreamReader("note.txt"); | // 创建读取器,打开 note.txt 来读 |
| 16 | Console.Write(sr.ReadToEnd()); | // ReadToEnd 把整个文件读成一个字符串 |
| 17 | sr.Close(); | // 关上 |
第一行 第二行
9小结
- 文件打开来读写时就成了流,流是一串字节。
- System.IO 里的类,一组搬运数据,一组管文件和文件夹。
- 创建 FileStream 要给文件名、FileMode、FileAccess 和 FileShare。
- 读写都从当前位置开始;读到末尾,
ReadByte返回 -1。
下一节:想给代码贴一张说明标签,比如告诉编译器“这个方法已经过时了,别再用”,该怎么写?
