本文转载连接地址: http://blog.csdn.net/shanyongxu/article/details/51039303
流
先想一个问题:将D:\文件夹下的图片headqq.bmp复制到C:\根目录下.暂时不要考虑是如何用代码实现,先考一个他的过程.
首先,程序运行在内存中,而文件位于磁盘中.
接下来,需要建立一个类似管道的东西将文件和内存中的应用程序连接起来,并且将文件按字节发送.注意:在应用程序中,为了保存接收到的文件字节,需要创建一个Byte[]数组.完成传递后,文件就以字节数组的方式保存在了内存中:
接下来就是C:\盘的headqq2.bmp文件中.过程正好和前面的相反现在C:\盘下创建一个空文件----headqq2.bmp:
然后建立起连接应用程序和文件的管道,接着讲字节写入到管道中,最后间接的再写入到文件中:
这个过程中,流就类似于上面的这个管道,他是一个抽象的概念.在C#中,流被实现为了Stream以及一系列的子类,同时还有一些装饰类和帮助类.流的最主要用途就是与应用程序外部的文件或数据源进行数据交互.这句话如果太抽象,换句话流的作用就是用来操作文件.比如在访问文件时,有文件流(FIleStream);在访问网络时,有网络流(NetworkStream);
在访问串口时,虽然没有流的子类型,但是SerialPort类型暴露出了BaseStream属性;在访问Web服务器时,HttpRequest和HttpResponse类型也分别包含了InputStream和OutputStream属性.简单说,流帮助我们与文件以及外围设备进行数据交流,因为流的主要用途是输入/输出,所以将它定义在了Stream.IO命名空间下.流与字节数组Byte[]不同,Byte[]是一个静态的数据容器,它本身保存了全部的数据;而流是一个动态的概念,按照字节的次序进行顺序访问,每次可以只访问单个字节,也可以访问连续的一段字节.
不管何种类型的流,都继承自基类Stream,因此它们的使用方式基本上是一致的,这也降低了大家学习的难度.上面是使用流最常见的案例,他有一个地方值得注意,那就是在应用程序中用于保存临时数据的Byte[]数组.
Byte[]数组的大小可以设置为与文件同样大小,然后一次性读取文件的全部字节,再将数据一次性的写入到新文件中.这样看上去没事,但是有两个问题:第一个是如果文件很大,比如2G(超过你的内存),那么将文件完全读取到应用程序的Byte[]数组中,无疑将占用巨大的内存;第二是如果文件位于远程服务器上,在传输开始时无法知道文件的大小,只有传输完毕之后才能知道.
此时,通常的做法是将Byte[]设置为固定的大小,比如1024字节,也就是1KB,然后将读取的操作写在一个循环中,每次先读取1KB,将1KB处理后转存到其他位置,直到读完文件中的所有数据.因为Byte[]保存的是临时数据,当下次循环时临时数据就会被覆盖丢弃.Byte[]将相当于一个临时缓存,因此通常会将它命名为buffer.
使用流进行文件复制
一次性复制
文件复制有更简洁的方法,但是这里谈论的是流,使用流的方式进行复制.
回想前面的东西,首先要做的是建立起应用程序和磁盘文件的联系,建立起他们之间的管道.在C#中,这通过创建一个文件流的对象来完成:
上面的语句创建了一个文件流对象.构造函数的第一个参数指定了文件的路径;第二个参数是一个枚举,指定操作系统打开文件的方式,对着这个按理来说,是打开现有文件,并从头读取,因此使用FIleMode.Open,类似的,还有FileMode.Create,FileMode.Append等;第三个参数表明了打开文件的意图,从名字就能开出,FileAccess.Read是进行读取,类似的,还有Write用于写入,ReadWrite用于读写.第二个参数和第三个参数可以随意搭配,如果第二个参数选择Append,表示向文件尾写入数据,就是所谓的追加,而第三个参数选择Read表示只读,则会引发异常.所以说,你在搭配的时候要考虑一下,别出现某些挺让人摸不着头脑的错误,你如果说你想吃草莓味的山楂,柠檬味的葡萄你这就是有点那个啥了.
然后,需要将文件通过流保存到应用程序中的Byte[]数组中.由于数组的长度是固定的,在声明时就要确定,好在Stream有一个Length属性以字节为单位表示流的长度.所以可以想下面这样声明:
对于本例中的文件流来说,可以直接访问length属性来获得文件的总字节数.但如同上面所讲的,并不是所有的流都可以访问Length属性.比如,文件位于远程服务器,通过网络流NetworkStream来访问它,这种情况下,访问Length属性总是抛出异常.
在声明了Byte[]数组之后,需要将文件以字节的方式读入到Byte[]中.这个步骤可以通过在Stream对象上调用Read()方法来完成.注意这里用到了一个很特别的词”读入”,它包含了两层意思,一个是”读”,指读取文件中的数据,一个是”入”,指将文件数据写入到Byte[]数组.如下:
Read()方法接受三个参数,第一个参数buffer,即要写入的字节数组;第二个参数是相对于文件头的偏移量,0的意思是代表从文件的头部开始读取.第三个参数表示读取的字节数.这里使用了一个强制类型转换,文件的字节数可以很大,比如一个ISO光盘镜像文件可以达到7GB以上,int类型无法表示这么大的数值,所以Length属性采用了long类型,如果将7GB的文件全部读取到内存中显然不现实,所以第三个参数的类型采用了int类型.因为int的最大值为2147483647,所以实际上一次最多只能读取2GB的数据.对于这个实例来说,显然可以满足.下面展示了int类型可以表示的最大字节数转换:
Read方法返回了读取到的字符数,通常等于Read()方法的第三个参数count.当返回0时,表示已经读到了文件末尾,也就是流的终点,至此就完成了将图片文件转换为Byte[]数组的所有工作.如果愿意,可以将byte[]数组中的内容显示出来,就得到了一串数字,这串数字就代表一个图片文件:
现在需要看看逆操作了,将应用程序中的Byte[]数组保存至文件.首先需要创建文件,并且创建文件流对象.不需要像上面那样先创建一个空文件,再去写入他.可以使用FileStream的重载方法来创建新文件,同时获得流对象:
在机器角度,文件是没有类型的,文件的后缀只是帮助操作系统决定由那个应用程序来打开,因为本示例中传输的是一个bmp图片文件,所以后缀名为bmp,实际上可以将后缀名命名为任何东西.
这里为啥要换成F盘了?因为貌似win7以上C盘需要提供权限,楼主这里为了简单,就换了.
上面的代码就是一个复制的小小的实现,当然还有很多的不足,比如如果路径不存在啊,各种类型的错误.
循环分批复制
上面的代码实现的是一次性复制,但是更多见的情况是传递一个更大的文件,或者事先无法得知文件大小(Length属性抛出异常),因此也就不能创建一个尺寸正好合适的byte[]数组,此时只能分批读取和写入,每次只读取部分字节,直到文件末尾,后面的内容多采用这种形式.
这次咱们复制一个大一点的文件,一首歌曲.
上面这段代码有三点需要注意的地方:
(1).定义了一个BufferSize变量,在声明数组和读取流时,使用BufferSize变量,而不是直接使用10240.这是为了避免出现前后不一致的情况,如果Read()方法的第三个参数(读取的字节数)超过了Byte[]数组的长度,在调用Write()时则会抛出异常.
(2)Read()和Write()方法的第二个参数一直保持为0.这个参数是读取或写入buffer时相对于数组第一个元素的偏移量,因为本示例中要写入或读出buffer中的全部字节,因此总是从0开始.
(3)当打开文件或创建文件时,流指针默认位于文件头,当调用Read()/Write()方法后,流指针会自动向后移动相应的字节数.因此在代码中无须设置,每次调用Read()和Write()时,读取或写入的都是尚未处理过的字节.
流的类型体系
前面咱们用到了和流相关的两个类:Stream和FileStream.很多人都觉得和流相关的类型很多很杂,其实是没有弄清楚这些类型之间的关系.可以将它们分为这样几类:
下面咱们一一介绍一下这几个流
1.基础流
流的类型体系的核心就是Stream,它定义了所有流都应该具备的行为,主要包括以下几种:
(1).从流中读取数据:CanRead,流是否可读;Read(byte[]buffer,int offset, int count),从当前流指针出读取count字节的数据,并写入到buffer中;ReadByte(),从流中读取一个字节.
(2)向流中写入数据:CanWrite,流是否可写;Write(byte []buffer,int offset,int count),在当前流指针处从buffer中读取count字节的数据,并写入到流中;WriteByte(Byte value),写入一个字节到流中.
(3)移动流指针,CanSeek,Seek(longoffset,SeekOrigin);流指针位置,Position;关闭流,Close(),Dispose();将缓存的字节立即写入存储设备,Flush();超时处理,CanTimeout,ReadTimeout,WriteTimeout;流长度,Length,SetLength(long value).
Stream是一个抽象类,这就意味着无法直接创建一个Stream的实例.除此之外,Stream类中的属性和方法都是抽象(abstract)和虚拟(virtual)的,这就意味着在Stream的子类中需要实现或重写他们.
基础流如下:
上图中继承了Stream的流都不是抽象类,之所以将它们称之为基础流,是因为上面每一种流的底层都对应了一种后备存储(backing store).对于FIleStream来说,对应的后备存储是文件;对于MemoryStream来说,对应的后备存储是内存;对于NetworkStream来说,后备存储是网络源.
上图中的IsolatedStorageFileStream继承了FIleStream,所以它的后备存储也是文件;还有MemoryStream这个流,它位于内存中.Byte[]数组也是位于内存中,那么要MemoryStream有啥用呢?MemoryStream提供了一个以流的方式来处理Byte[]数组的方法,可以简单的认为MemoryStream使操作Byte[]数组的方式变得更加丰富.
2.装饰器流
装饰器流区别于基础流的特点主要有下面两点:
(1).装饰器流实现了Decorate模式,它们包含了一个Stream流基类的引用,同时继承自Stream基类.Decorate翻译成中文是装饰器,因此将它们叫做装饰器流.Decorate模式的作用是为现有类添加功能,因为装饰器是基于Stream基类的,而不是特定的FileStream或MemoryStream,因此它可以为所有的流添加功能.
(2)因为装饰器流是基于Stream基类的,所以他没有后备存储的概念,并不对应一个文件,一段内存区域或一个网络端口.它可以应用于所有的流类型.
装饰器流如下所示:
这些装饰器流并没有全部定义在System.IO命名空间下.DeflateStream和GZipStream位于System.IO.Copression,用于压缩和解压缩;CryptoStream位于System.Security.Cryptography,用于加密解密;AuthenticatedStream位于System.NET.Security,用于安全性.只有BufferedStream位于SYstem.IO下,用于增强缓存.
3.包装器类
大家应该先明白关于流的最常用的方法:数据传输.当然更多的时候,读取一个文件并不是为了将它复制到另一个地方,而是读取和处理文件的内容.当文件内容从文件中经过流传递到应用程序中之后,变成了一串数字组成的Byte[]数组,那么要如何读取文件内容呢?和流相关的一组包装器类提供了这个服务,它可以方便的协助开发者处理流所包含的数据,并且不需要将流转存为Byte[]数组的形式.能明白我的意思吧?
看看题目,是类而不是流.说明这已经不是流类型了,但是这些类型包含了一个流的引用,提供了对流进行操作的简便方法,相当于一个包装器(Wrapper).包装器包含两组:
每组包装类都包含了两个类型,Reader后缀的类型用于读取,Write后缀的类型用于写入.
(1)StreamReader和StreamWrite
StreamReader继承自TextReader,用于将流中的数据读取为字符.StreamWrite则相反,用于将字符写入到流中.值得一提的是,虽然TextReader和TextWrite分别是StreamReader和StreamWrite的基类,但是和流一点关系都没有,它只是定义了一组通用的,读取和写入字符数据的方式.在.NET中,还有两个类型,StringReader和StringWrite,他们也继承了TextReader和TextWrite,但是它们用于处理字符串,而不是流.
现在假设上面text中的字符串定义在一个文本文件中,只需要简单的将上面的StringReader换成StreamReader就可以了,甚至不需要对其余的代码做任何修改.上面的text变量声明的字符串保存在应用程序根目录下的about.text文本文件中.将StringReader的声明换成下面语句.
凡是涉及文本文件的,就不可避免的遇到编码方式的问题.简单的说,编码方式定义了字节如何转换成人类刻度的字符或者文本,可以将它想象成一个字节和字符的对应关系表.如果文件创建时使用一种编码方式,比如GB2312,读取时又采取了另一种编码方式,比如UTF-8编码,那么就不能转换为正确的自负了,将会得到一堆乱码.上面代码将编码方式指定为了GB2312,正是因为在创建文件时,将其保存为了GB2312格式.关于如何查看文本格式的内容这里不多说了.
上例中,在创建StreamReader实例时,为它的构造函数传递了一个FileStream对象.StreamReader类还有一些重载方法,可以将这个过程变得更简洁,比如直接传递一个文件路径:
注意这里没有指定编码方式默认是使用UTF-8.所以上面的语句相当于:
StreamReader reader = new StreamReader("about.txt",Encoding.UTF8);
StreamWrite的使用方法也同样简单,这里只进行一个简单的演示:
(2).BinaryReader和BinaryWrite
StreamReader和StreamWrite分别适用于读取和写入文本字符的场合,而BinaryReader和BinaryWrite的适用范围更加广泛.BinaryWrite用于向流中以二进制方式写入基元类型,例如int,float,char,string等;BinaryReader则用于从流中读取基元类型.他们是独立的类,不继承字TextReader和TextWrite.
案例如下:使用BinaryReader和BinaryWrite的使用方式
测试代码如下:
你也可以打开product.txt文件看看.可能会出现乱码的情况.
4.帮助类
流体系中的最后一种是帮助流,这些帮助流与流的关系其实已经不那么密切了,但是它们又能够使对文件流的操作变得简单,并且和Stream类一样,也位于System.IO命名空间下,因此这里也将它们归为流的体系中.这些帮助流有File和FileInfo.
File是一个静态类,提供了对文件的快速操作,比如,上例中创建一个文件时使用File会更加简单:
打开文件也是一样,FIle提供了Open(String Path,FileMode mode),OpenRead()和OpenWrite()几个方法,从方法名就可以看出方法的用法.除此之外,File还提供了其他一些快捷操作的方法,例如ReadAllText()以文本方式读取文件的全部内容,ReadAllByte()以字节方式读取文件全部内容.与读取项对象,还有写入的方法,例如WriteAllBytes(),WriteAllLines()等.如果想复制文件,只需要调用File类中的Copy(String sourceFileName,string destFileName).
所以说,对于文件,大家几乎用不着手动创建FileStream和StreamReader/StreamWriter,应该优先考虑File静态类中快速方法.
FileInfo和File是相对应的,只不过它是一个普通类,通过创建一个FileInfo的实例对象来进行类似的操作.
System.IO下还有其他一些类也很有用,这里就不一一介绍了,比如Path,用于处理路径;Directory类和DirectoryInfo类,与FIle和FileInfo的关系类似,只不过它们是用来处理文件夹的.