1 文件系统概述
C#将文件视为一个字节序列,以流的方式对文件进行操作。流是字节序列的抽象概念,文件、输入/输出设备、内部进程通信管理以及TCP/IP套接字等都可以视为一个流。.NET对流的概念进行了抽象,为这些不同类型的输入和输出提供了统一的视图,使程序员不必了解操作系统和基础设备的具体细节。
文件和流既有区别又有联系。文件是在各种驱动器上永久或临时存储的数据的有序集合,是进行数据读写操作的基本对象。
从概念上讲,流非常类似于单独的磁盘,同时也是进行数据读取操作的对象。流提供了连续的字节存储空间,通过流可以向后备存储器写入数据以及从后备存储器读取数据。虽然数据实际的存储位置可以不连续,甚至可以分布在多个磁盘上 ;但是用户看到的是封装以后的数据结构,是连续的字节流。和磁盘文件直接相关的流叫做文件流,流还有其它多种类型,如网络流、内存流和磁盘流等。
文件中的数据可以有不同的编码格式,最根本的两种就是ASCΙΙ编码和二进制编码。采用ASCΙΙ编码的文件又称为文本文件,它的每一个字节对应一个ASCΙΙ码,代表一个字符。二进制文件则是把内存中的二进制数据按原样写入文件。例如整数10000在内存中占2个字节,那么它在二进制文件中也只占2个字节;而以ASCΙΙ文件保存时每位数字各占1个字节,共5个字节。使用ASCΙΙ文件格式便于处理字符,编程也较简单,但占用的存储空间和处理时间较多;使用二进制文件格式则更节省空间和时间。
.NET类库的System.IO命名空间中提供了完整的对文件和流的访问支持。利用它所提供的功能,可以在程序中同步或异步地访问数据文件和数据流。这些文件和流既可以存储在本地硬盘上,也可以存储在远程机器上,或者是移动设备中,甚至可以存在于某个内存地址或网络通道中。
2 驱动器、目录和文件
.NET类库中提供了DriveInfo类、Directory类和File类,分别对驱动器、目录和文件进行了封装。这3个类都是密封类,无法从中派生中其它类;而且,Directory和File类还属于抽象类,无法创建它们的实例,而只能通过类的原型调用其公有的静态方法成员。
2.1 与IO操作相关的枚举
首先介绍一些在文件IO操作中常用到的枚举定义。
2.1.1 FileAccess
该枚举类型表示对文件的访问权限,可以是以下值:
• Read,对文件拥有读权限;
• ReadWrite,对文件同时拥有读写权限;
• Write,对文件拥有写权限。
2.1.2 FileAttributes
该枚举类型表示文件的类型,可以是以下值:
• Archive 存档文件
• Compressed 压缩文件
• Device 设备文件
• Directory 目录
• Encrypted 加密文件
• Hidden 隐藏文件
• Normal 普通文件
• NotContentIndexed 无索引文件
• Offline 脱机文件
• ReadOnly 只读文件
• ReparsePoint 重分析文件
• SparseFile 稀疏文件
• System 系统文件
• Temporary 临时文件
上述枚举值可以按位组合使用。但不能是相同排斥的,比如一个文件不能既是普通文件又是隐藏文件。
2.1.3 FileMode
该枚举类型表示文件的打开方式
• Append 以追加方式打开文件,如果文件存在则到达文件末尾,否则创建一个新文件;
• Create 创建并打开一个新文件,如果文件已经存在则覆盖旧文件;
• CreateNew 创建并打开一个新文件,如果文件已经存在发生异常
• Open 打开现有文件,如果文件不存在发生异常;
• OpenOrCreate 打开或者新建一个文件,如果文件已经存在则打开它,否则创建并打开一个新文件;
• Truncate 打开现有文件,并清空文件内容。
2.1.4 FileShare
该枚举类型表示文件的共享方式
• None 禁止任何形式的共享
• Read 读共享,打开文件后允许其它进程对文件进行读操作
• ReadWrite 读写共享,打开文件后允许其它进程对文件进行读和写操作
• Write 写共享,打开文件后允许其它进程对文件进行写操作。
2.1.5 SeekOrign
该枚举类型表示以什么为基准来表示文件流中的偏移量
• Begin 从文件流的起始位置计;
• Current 从文件流的当前位置计;
• End 从文件流的结束位置计
2.1.6 NotifyFilters
该枚举类型用于指定对文件或目录中哪些属性的修改进行监视
• Attributes 对属性的变化进行监视
• CreationTime 对创建时间的变化进行监视
• DirectoryName 对目录名称的变化进行监视
• FileName 对文件名称的变化进行监视
• LastAccess 对最后一次访问时间的变化进行监视
• LastWrite 对最后一次写入时间的变化进行监视
• Security 对安全性设置的变化进行监视
• Size 对文件大小进行监视
2.1.7 DriveType
该枚举类型用于定义与驱动器类型有关的常量
• CDRom CD-ROM驱动器;
• Fixed 固定磁盘驱动器;
• NetWork 网络驱动器;
• NoRootDirectory 不含根目录的驱动器;
• Ram RAM闪盘驱动器;
• Removable 可移动存储设备;
• Unknown 驱动器设备类型未知。
2.2 驱动器
通过DriveInfo类可以访问某个驱动器的相关信息。在创建一个DriveInfo对象时,需要将指定的盘符传递给该类的构造函数。盘符的范围是从字母a到z,与大小写无关。
(参见DriveInfo类的公有属性)
属性名 | 类型 | 含义 |
AvailableFreeSpace | long | 驱动器上的剩余可用空间 |
DriveFormat | string | 驱动器上的文件系统格式 |
DriveType | DirveType | 驱动器的类型 |
IsReady | bool | 驱动器是否已准备好(针对软驱、CD-ROM、可移动设备等) |
Name | string | 驱动器的名称 |
RootDirectory | string | 驱动器上的根目录 |
TotalFreeSpace | long | 驱动器上的总的剩余空间 |
TotalSize | long | 驱动器上总的空间 |
VolumeLable | string | 驱动器的卷标 |
在DriveInfo属性中,除了驱动器卷标VolumeLabel属性是可以设置的以外,其它的属性都是只读的。所有与磁盘空间相关的属性,其值都以字节为单位。属性AvailableFreeSpace和TotalFreeSpace的差别在于前者将磁盘配额考虑在内。
如果IsRead属性值为false,那么在读取AvailableFreeSpace、DriveFormat、TotalFreeSpace、TotalSize和VolumeLabel这些属性时,将引发一个IOException异常。在访问VolumeLabel属性时,如果访问者没有足够的权限,将引发一个SecurityException异常。
DriveInfo类还提供了一个公有的静态方法GetDrives,该方法返回一个DriveInfo[]类型数组,表示当前计算机上所有逻辑驱动器的列表。如果该方法的调用者没有足够的权限,将引发一个UnauthorizedAccessException异常。
2.3 目录
使用Directory类提供的目录管理功能,可以创建、移动和删除目录,还可以获取和设置目录的有关信息。
在使用目录和文件的路径时,一定要使用转义符“\\”来替代字符串中的字符“\”。
2.4 文件
使用File类提供的文件管理功能,不仅可以创建、拷贝、移动和删除文件,还可以打开文件,以及获取和设置文件的有关信息。
使用文件对象时,当另一个程序或进程正在使用文件时,对文件的读写、移动等操作都会失败;在使用完文件对象之后,也一定要注意关闭文件。
和目录类似,.NET类库中也提供了一个FileInfo类,其功能与File类有很多重叠的地方,比如对文件的创建、修改、拷贝、移动、删除等,以及创建新的流对象。
3 文件流和数据流
不同的流可能有不同的存储介质,比如磁盘、内存等。.NET类库中定义了一个抽象类Stream,表示对所有流的抽象,而每种具体的存储介质都可以通过Stream的派生类来实现自己的流操作。
FileStream是对文件流的具体实现 ,通过它可以以字节方式对流进行读写,这种方式是面向结构的,控制能力较强,但使用起来稍显麻烦。
此外,System.IO命名空间提供了不同的读写器来对流中的数据进行操作,这些类通常成对出现,一个用于读,另一个用于写。例如:TextReader和TextWriter以文本方式 (ASCΙΙ)对流进行读定;而BinaryReader和BinaryWriter采用的是二进制方式。TextReader和TextWriter都是抽象类,它们各有两个派生类:StreamReader、StringReader以及StreamWriter、StringWriter.
3.1 抽象类Stream
Stream支持同步和异步的数据读写。它和它的派生类共同组成了.NET Framework上IO操作的抽象视图,这使得开发人员不必去了解IO操作的细节,就能够以统一的方式处理不同介质上的流对象。
Stream的4个布尔类型的属性(CanRead、CanWrite、CanSeek、CanTimeout)都是只读的。一旦建立了一个流对象之后,流的这些特性就不能被修改了。由于流是以序列的方式是对数据进行操作,因而支持长度和当前位置的概念。在同步操作中,一个流对象只有一个当前位置,不同的程序或进程都在当前位置进行操作;而在异步操作中,不同的程序或进程可以在不同的位置上进行操作,当然这需要文件的共享支持。最后,流的超时机制是指在指定的时间限制内没有对流进行读或写操作,当前流对象将自动失效。
Stream类提供的公有方法则用于流的各项基本操作。
新建一个流时,当前位置位于流的开始,即属性Position的值为0。每次对流进行读写,都将改变流的当前位置。可以将流的当前位置理解成“光标”的概念。读操作从流的当前位置开始,读入指定的字节数,光标就向后移动对应的字节数。写操作也是同理。
根据需要,可以使用Position属性或Seek方法来改变流的当前位置。不过Position属性是指流的绝对位置,即从流的起始位置开始计算。Seek方法则需要通过SeekOrigin枚举类型来指定偏移基准,即是从开始位置、结束位置还是当前位置进行偏移。如果代定为SeekOrgin.End,那么偏移量为负数,表示将当前位置向前移动。
如果指一的读写操作位置超出了流的有效范围,将引发一个EndOfStreamException异常。
3.2 文件流FileStream
FileStream支持同步和异步文件读写,也能够对输入输出进行缓存以提高性能。
FileStream类提供了多达14个构造函数,能够以多种方式来构造FileStream对象,并在构造的同时指定文件流的多个属性。当前部分构造函数是为了兼容旧版本的程序而保留的。对于文件的来源,可以使用文件路径名,也可以使用文件句柄来指定。以文件路径名为例,构造FileStream对象时至少需要指定文件的名称和打开方式两个参数,其它如文件的访问权限、共享设置以及使用的缓存区大小等,则是可选的;如不指定则使用系统的默认值,如默认访问权限为FileAccess.ReadWrite,共享设置为FileShare.Read。
下面的代码以只读方式打开一个现有文件,并且在关闭文件之前禁止任何形式的共享。如果文件不存在,将引发一个FileNotFoundException:
FileStream fs = new FileStream("c:\\MyFile.txt",FileMode,Open,FileAccess.Read,FileShare.None); fs.Close();
也可以使用File的静态方法来获得文件流对象。File类的静态方法Open和FileStream构造函数的参数类型基本一致。例如上面的代码等价于:
FileStream fs = File.Open("C:\\MyFile.txt",FileMode.Open,FileAccess.Read,FileShare.None); fs.Close();
File类的静态方法OpenRead和OpenWrite也能够返回一个FileStream对象,但它们只接受文件名这一个参数。对于OpenRead方法,文件的打开方式为FileMode.Open,共享设置为FileShare.Read,访问权限为FileAccess.Read;而对于OpenWrite方法,打开方式为FileMode.OpenOrCreate,共享设置为FileShare.None,访问权限为FileAccess.Write。下面两行代码是等价的:
FileStream fs = new FileStream("c:\\MyFile.txt",FileMode.OpenOrCreate,FileAccess.Write,FileShare.None); FileSream fs = File.OpenWrite("c:\\MyFile.txt");
FileStream类的ReadByte和WriteByte方法都只能用于单字节操作。要一次处理一个字节序列,需要使用Read和Write方法,而且读写的字节序列都位于一个byte数组类型的参数中。看下面的程序:
class FileStreamSample { static void Main() { //创建一个文件流 FileStream fs = new FileStream("c:\\MyFile.txt", FileMode.Create); //将字符串的内容放入缓冲区 string str = "Welcome to the Garden!"; byte[] buffer = new byte[str.Length]; for (int i = 0; i < str.Length; i++) { buffer[i] = (byte)str[i]; } //写入文件流 fs.Write(buffer, 0, buffer.Length); string msg = ""; //定位到流的开始位置 fs.Seek(0, SeekOrigin.Begin); //读取流中的前7个字符 for (int i = 0; i < 7; i++) { msg += (char)fs.ReadByte(); } //显示读取的信息和流的长度 Console.WriteLine("读取内容为:{0}", msg); Console.WriteLine("文件长度为:{0}", fs.Length); //关闭文件流 fs.Close(); } }
使用完FileStream对象后,一定不能忘记使用Close方法关闭文件流,否则不仅不会使别的程序不能访问文件,还可能导致文件损坏。程序输出:
读取内容为:Welcome 文件长度为:22 请按任意键继续. . .
3.3 流的文本读写器
StreamReader和StreamWriter主要用于以文本方式对流进行读写操作,它们以字节流为操作对象,并支持不同的编码格式。
StreamReader和StreamWriter通常成对使用,它们的构造函数形式也一一对应。可以通过指定文件名或指定另一个流对象来创建StreamReader和StreamWriter对象。还可以指定文本的字符编码、是否在文件头查找字节顺序标记,以及使用缓存区大小。
文本的字符编码默认为UTF-8格式。在命名空间System.Text中定义的Encoding类对字符编码进行了抽象,它的5个静态属性分别代表了5种编码格式:
• ASCΙΙ
• Default
• Unicode
• UTF-7
• UTF-8
Encoding类的Default属性表示系统的编码,默认为ANSI代码页,这和StreamReader和StreamWriter中默认的UTF-8编码是不一样的。通过StreamReader和StreamWriter类的公有属性Encoding可以获得当前使用的字符编码。StreamReader类还有一个布尔类型的公有属性EndOfStream,用于指定读取的位置是否已经到达流的末尾。
下面的代码从一个文件流构造了一个StreamReader对象和StreamWriter对象,还为StreamWriter对象指定了Unicode字符编码。不过在实际使用中,为同一文件进行读写操作所构造的两个对象通常使用同样的字符编码格式:
FileStream fs = new FileStream("c:\\MyFile.txt",FileMode.Create); StreamReader sr = new StreamReader(fs); StreamWriter sw = new StreamWriter(fs,System.Text.Encoding.Unicode); sw.Close(); sr.Close(); fs.Close();
注意在关闭文件时,要先关闭读写器对象,再关闭文件流对象。如果对同一个文件同时创建了StreamReader和StreamWriter对象,则应先关闭StreamWriter对象,再关闭StreadReader对象。否则将引发ObjectDisposedException异常。
即使是直接使用文件名来构造StreamReader或StreamWriter对象,或是使用File类的静态方法OpenText和AppendText来创建StreamReader或StreamWriter对象,过程当中系统都会自动生成隐含的文件流,读写器对文件的读写还是通过流对象进行的。该文件流对象可以通过StreamReader或StreamWriter对象的BaseStream属性获得。
不通过文件流而直接创建StreamReader对象时,默认的文件流对象是只读的。以同样的方式来创建StreamWriter对象的话,默认的文件流对象是只写的。
class BaseStreamSample { static void Main() { StreamReader sr = new StreamReader("c:\\MyFile.txt"); Console.WriteLine("CanRead:" + sr.BaseStream.CanRead); Console.WriteLine("CanWrite:" + sr.BaseStream.CanWrite); sr.Close(); StreamWriter sw = new StreamWriter("c:\\MyFile.txt"); Console.WriteLine("CanRead:" + sw.BaseStream.CanRead); Console.WriteLine("CanWrite:" + sw.BaseStream.CanWrite); sw.Close(); } }
CanRead:True
CanWrite:False
CanRead:False
CanWrite:True
请按任意键继续. . .
由于使用的是不同的流对象,此时就不能同时使用StreamReader和StreamWriter对象来打开同一个文件。在上例中,如果不关闭StreamReader对象就创建StreamWriter对象,将引发一个IOException异常。使用File类的静态方法OpenText和AppendText时,情况也一样。
StreamReader中可以使用4种方法对流进行读操作
• Read,该方法有两种重载形式,在不接受任何输入参数时,它读取流的下一个字符;当在参数中指定了数组缓冲区、开始位置和偏移量时,它读入指定长度的字符数组。
• ReadBlock,从当前流中读取最大数量的字符,并将数据输出到缓冲区。
• ReadLine,从当前流中读取一行字符,即一个字符串。
• ReadToEnd,从流的当前位置开始,一直读取到流的末尾,并把所有读入内容都作为一个字符串返回;如果当前位置位于流的末尾,返回空字符串。
StreamReader最常用的是ReadLine方法,该方法一次读取一行字符。这里的“行”的定义是指一个字符序列,该序列要么以换行符(“\n”)结尾,要么以换行回车符(“\r\n”)结尾。
StreamWriter则提供了Write和WriteLine方法对流进行写操作。不过这两个方法可以接受的参数类型则丰富得多,包括char、int、string、float、double及至object等,甚至可以对字符串进行格式化:
FileStream fs = new FileStream("c:\\MyFile.txt", FileMode.Create, FileAccess.Write); StreamWriter sw=new StreamWriter (fs); sw.WriteLine(25); sw.WriteLine(0.25f); sw.WriteLine(3.1415926); sw.WriteLine('A'); sw.WriteLine("写入时间:"); int hour = DateTime.Now.Hour; int minute = DateTime.Now.Minute; int second = DateTime.Now.Second; sw.WriteLine("{0}时{1}分{2}秒", hour, minute, second); sw.Close(); fs.Close();
得到的文本文件内容是:
25 0.25 3.1415926 A 写入时间: 14时39分28秒
Write和WriteLine方法所提供的重载形式和Console.Write以及Console.WriteLine方法完全一样。实际上上写入任何类型的对象时,都调用了对象的ToString方法。
StringReader和StringWriter同样是以文本方式对流进行IO操作,但它们以字符串为操作对象,功能相对简单,且只支持默认的编码方式。
3.4 流的二进制读写器
BinaryReader和BinaryWriter以二进制方式对流进行IO操作。它们的构造函数中需要指定一个Stream类型的参数,如有必要还可以指定字符的编码格式。和文本读写器不同的是:BinaryReader和BinaryWriter对象不支持从文件名直接进行构造。
类似的,可以通过BinaryReader和BinaryWriter对象的BaseStream属性来获得当前操作的流对象。
BinaryReader类提供了多个读操作方法,用于读入不同类型的数据对象,这些方法请参见MSDN。
使用这些方法时,注意,方法名称中指代的都是数据类型在System空间的原型。例如读取单精度浮点数值,方法名称是ReadSingle而不是ReadFloat,另外读取short、int、long类型的整数值,方法名称分别是ReadInt16、ReadInt32、ReadInt64。
而BinaryWriter则只提供了一个方法Write进行写操作,但提供了多种重载形式,用于写入不同类型的数据对象。各种重载形式中的参数类型和个数与StreamWriter中基本相同。
FileStream fs = new FileStream("c:\\MyFile.bin", FileMode.OpenOrCreate); BinaryWriter bw = new BinaryWriter(fs); BinaryReader br = new BinaryReader(fs); bw.Write(25); bw.Write(0.5f); bw.Write(3.1415926); bw.Write('A'); bw.Write("写入时间"); bw.Write(DateTime.Now.ToString()); //定位到流的开始位置 fs.Seek(0, SeekOrigin.Begin); //依次读出各类型数据 int i = br.ReadInt32(); float f = br.ReadSingle(); double d = br.ReadDouble(); char c = br.ReadChar(); string s = br.ReadString(); DateTime dt = DateTime.Parse(br.ReadString()); Console.WriteLine("int:{0},float:{1}", i, f); //关闭文件 bw.Close(); br.Close(); fs.Close();
3.5 常用其它流对象
除FileStream类之外,代表具体流的、Stream类的常用派生类还有:
(1)MemoryStream,表示内存流,支持内存文件的概念,不需要使用缓冲区;
(2)UnmanagedMemoryStream,和MemoryStream类似,但支持从可控代码访问不可控的内存文件内容;
(3)NetworkStream,表示网络流,通过网络套接字发送和接收数据,支持同步和异步访问,但不支持随机访问;
(4)BufferStream,表示缓存流,为另一个流对象维护一个缓冲区;
(5)GZipStream,表示压缩流,支持对数据流的压缩和解压缩;
(6)CryptoStream,表示加密流,支持对数据流的加密和解密。
同样,可以由这些流对象构造出文本读写器或二进制读写器,并进行相应方式的读写操作。
下面的程序演示了利用文件流和缓存流来共同维护一个三角函数表:
class BufferedStreamSample { static void Main() { TriangleTable table = new TriangleTable(); Console.WriteLine("请输入度数(0~179之间):"); try { int x = int.Parse(Console.ReadLine()); Console.WriteLine("请选择函数类型:0.正弦函数 1.余弦函数 2.正切函数 3.余切函数"); int iType = int.Parse(Console.ReadLine()); table.Open(); double y = table.GetFunction(x, iType); Console.WriteLine("函数值 = {0}", y); } catch (Exception) { File.Delete("c:\\Triangle.tbl"); } finally { table.Close(); } } } public delegate double TwoIntFunction(int param1,int param2); public class TriangleTable { private string tbl_Name="c:\\Triangle.tbl"; private FileStream m_baseStream; private BufferedStream m_Stream; private BinaryReader m_reader; public TwoIntFunction GetFunction; public TriangleTable() { if (!File.Exists(tbl_Name)) { m_baseStream = new FileStream(tbl_Name, FileMode.Create); BinaryWriter writer = new BinaryWriter(m_baseStream); byte[] buf = new byte[8]; for (int i = 0; i < 180; i++) { double sin = Math.Sin(Math.PI * i / 180); double cos = Math.Sqrt(1 - sin * sin); double tan = sin / cos; double ctan = cos / sin; writer.Write(sin); writer.Write(cos); writer.Write(tan); writer.Write(ctan); } writer.Close(); m_baseStream.Close(); } } public void Open() { m_baseStream = new FileStream(tbl_Name, FileMode.Open); m_Stream = new BufferedStream(m_baseStream); m_reader = new BinaryReader(m_Stream); GetFunction = delegate(int angle, int iType) { if (iType < 0 || iType > 3) throw new ArgumentOutOfRangeException("参数应为0~3之间的整数"); m_Stream.Seek(sizeof(double) * (4 * angle + iType), SeekOrigin.Begin); return m_reader.ReadDouble(); }; } public void Close() { m_reader.Close(); m_baseStream.Close(); m_Stream.Close(); } }
在首次使用三角函数表类TriangleTable时,计算出三角函数表所维护的全部数据,并存放在一个文件中,以后每次使用该类,都将数据读到缓存流,并通过缓存流直接获取函数值。
4 程序示例