在.NET Framework中进行的所有的输入和输出工作都要使用到流(stream)。流是序列化设备(serial device)的抽象表示。序列化设备可以以线性方式存储数据,并可以按同样的方式访问:一次访问一个字节。
有两种类型的流:
- 输出流: 当向某些外部目标写入数据时,就要用到输出流。这可以是物理磁盘文件、网络位置、打印机或另一个程序。
- 输入流: 用于将数据读入程序可以访问的内存或变量中。到目前为止,我们使用的最常见的输入流形式是键盘。
流的相关接口和类主要位于System.IO
命名空间,其中Stream
类是抽象类,它有三个重要子类分别针对的是不同的存取对象:FileStream
类表示文件操作,MemoryStream
表示内存操作,BufferedStream
类表示缓冲处理。
1.Stream类
抽象的Stream
类包含了流中所需要的许多属性和方法,如下表:
这些属性和方法中涉及了流的读写的各个方面。读写操作的4个方法如下:
int ReadByte();
int Read(byte[] array, int offset, int count);
void WriteByte(byte value);
void Write(byte[] array, int offset, int count);
其中ReadByte()
将读入的字节转成整数并返回,如果没有读到字节,则返回-1
。Read()
方法返回所读字节的数目。
通过BeginRead()
、EndRead()
、BeginWrite()
和EndWrie()
等方法,Stream类可以支持异步I/O 操作。
Seek()
方法表示在流中对搜索指针进行定位,用来决定下一步的读或写操作的位置。在这样的流中,其CanSeek
属性值为true
,并且可以使用其Seek()
方法来设定指针的位置。Seek()
方法需要两个参数:一个用来表示搜索指针移动距离的数值,另一个用来确定指针移动的参照位置。参照位置是SeekOrigin
的枚举成员,可以是以下3中情况之一:
- ①
SeekOrigin.Begin
(文件的开头); - ②
SeekOrigin.Current
(文件中指针的当前位置); - ③
SeekOrigin.End
(文件的结尾)。
下面代码展示了如何在文件中进行搜索处理:
aStream.Seek(200, SeekOrigin.Begin); // 从开头移到200位置
aStream.Seek(0, SeekOrigin.End); // 移到文件尾
aStream.Seek(-20, SeekOrigin.Current); // 从当前位置反向移动20
2.FileStream类
FileStream
是从Stream
中直接派生而来的。FileStream
对象既可以从文件中读出内容,也可以向文件中写人内容,并且可以处理字节、字符、字符串以及其他一些数据类型。该对象也可以被用来执行标准的输入/输出及标准错误的输出。
应该注意FileStream
对象通常不单独使用,因为其应用比较接近于底层。该对象只能对字节进行读写操作,因此在使用时必须把字符串、数字以及对象都转换成字节才能将其传递到FileStream
中。鉴于此,FileStream
通常被包装到其他一些类中加以使用,如BinaryWriter
或者TextReader
,这些类可以处理高层的数据结构。FileStream类具有很多形式的构造方法,因而可以根据以下这些参数的不同组合采用不同的FileStream
构造方法:
- ① 文件名;
- ② 文件句柄——用来表示文件句柄的一个整数;
- ③ 访问模式——FileMode 枚举值之一;
- ④ 读/写权限——FileAccess 枚举值之一;
- ⑤ 共享模式——FileShare 枚举值之一;
- ⑥ 缓冲器大小。
表6-3、表6-4和表6-5 分别说明了文件的访问模式、访问权限以及共享模式。
例如,要创建一个文件,并使该文件支持共享的读操作:
FileStream fs = new FileStream(@"c:\temp\foo.txt", FileMode.Create, FileAccess.Reead);
FIleStream
可以通过同步或者异步方式创建,同时,除了从Stream
中继承的属性之外,还增加IsAsync
属性。并且,该类中还增加下表列出的方法:
GetHandle()
方法能够返回一个可以用于本地操作系统函数(如Win32中的ReadFile()
)的标识符,但该方法一定要慎用。如果使用文件句柄对基础文件做了某些改动,然后又试图在该文件上使用FileStream
,则有可能会破坏文件中的数据。
FileStream
类在操作时,可能会产生以下几种异常:
- ① ArgumentException——路径为空字符
- ② ArgumentNullException——路径是一个null引用
- ③ SecurityException——对文件没有操作权限
- ④ FileNotFoundException——找不到文件
- ⑤ IOException——发生了一些其他的I/O错误,如指定了一个错误的驱动符
- ⑥ DirectoryNotFoundException——目录不存在
例,通过FileStream
来读写文件的内容:
using System;
using System.IO;
class Test
{
static void Main(){
try {
FileStream fsw = new FileStream("test.dat", FileMode.Create, FileAccess.Write);
// Wirte some data to the stream
fsw.WriteByte(33);
fsw.Write(new byte[] {
34, 35, 36 }, 0, 3);
fsw.Close();
FileStream fsr = new FileStream("test.dat", FileMode.Open, FileAccess.Read);
Console.WriteLine(fsr.ReadByte());
Console.WriteLine(fsr.ReadByte());
Console.WriteLine(fsr.ReadByte());
Console.WriteLine(fsr.ReadByte());
} catch (Exception e) {
Console.WriteLine("Exception:" + e.ToString());
}
}
}
运行结果:
3.MemoryStream类
MemoryStream
类也是从Stream
中直接继承而来的,它使用内存代替文件来存储流,但其处理与FileStream
非常类似。MemoryStream
把数据以字节数组的形式存储在内存中,并且可以用来代替应用程序中临时文件的作用。
如同FileStream
一样,MemoryStream
也有很多构造方法。其中以下两种比较常用:
MemoryStream();
MemoryStream(byte []);
用这里的第一个构造方法建立MemoryStream
,当向流的末尾写入数据时,MemoryStream
可以随之扩张。用第二个构造方法所建立的是基于指定字节数组的MemoryStream
类的新实例,这样建立的流无法调整大小。
除了从Stream
继承的属性外,MemoryStream
还增加了一个Capacity
属性。Capacity
属性可以用来指出当前分配到流上的字节数。当使用基于字节数组的流时,这一属性是非常有用的,因为该属性可以告知数组的大小,而Length
属性则可以指出当前正被使用的字节数。
MemoryStream
不能够执行异步的读/写方法,因为对内存的I/O不需要这种特性。但该对象可以执行下面3种附加方法。
- ①
GetBuffer()
——返回对流中的字节数组的一个引用 - ②
ToArray()
——将所有内容写入到字节数组中 - ③
WriteTo()
——将流中的内容写入到另一个Stream中
例,使用MemoryStream对内存进行操作:
using System;
using System.IO;
class Test
{
static void Main(){
try {
byte[] ary = {
33, 34, 35, 36, 37 };
int b;
MemoryStream msr = new MemoryStream(ary);
MemoryStream msw = new MemoryStream();
while ((b = msr.ReadByte()) != -1) {
msw.WriteByte((byte)(b + 3));
}
byte[] result = msw.ToArray();
foreach (byte bt in result)
Console.WriteLine(bt);
} catch (Exception e) {
Console.WriteLine("Exception:" + e.ToString());
}
}
}
运行结果:
4.BufferedStrea类
BufferedStream
可以提高读写操作的执行效率,因为该类可以把数据缓存到内存中,从而减少了对操作系统的调用次数。BufferedStream
不能够单独使用,而应该将其包装到流的其他一些类型中,特别是下面所描述的BinaryWriter
和BinaryReader
类型的流中。另外,将网络流(NetworkStream)进行缓存包装也是常见的。对于BinaryStream
调用Flush()
操作可以让缓存的内容真正地写到流中。