来源:互联网 发布:自动变速箱测试软件 编辑:程序博客网 时间:2024/05/16 12:18

本文转自: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服务器时,HttpRequestHttpResponse类型也分别包含了InputStreamOutputStream属性.简单说,流帮助我们与文件以及外围设备进行数据交流,因为流的主要用途是输入/输出,所以将它定义在了Stream.IO命名空间下.流与字节数组Byte[]不同,Byte[]是一个静态的数据容器,它本身保存了全部的数据;而流是一个动态的概念,按照字节的次序进行顺序访问,每次可以只访问单个字节,也可以访问连续的一段字节.

 

不管何种类型的流,都继承自基类Stream,因此它们的使用方式基本上是一致的,这也降低了大家学习的难度.上面是使用流最常见的案例,他有一个地方值得注意,那就是在应用程序中用于保存临时数据的Byte[]数组.

 

Byte[]数组的大小可以设置为与文件同样大小,然后一次性读取文件的全部字节,再将数据一次性的写入到新文件中.这样看上去没事,但是有两个问题:第一个是如果文件很大,比如2G(超过你的内存),那么将文件完全读取到应用程序的Byte[]数组中,无疑将占用巨大的内存;

第二是如果文件位于远程服务器上,在传输开始时无法知道文件的大小,只有传输完毕之后才能知道.

 

此时,通常的做法是将Byte[]设置为固定的大小,比如1024字节,也就是1KB,然后将读取的操作写在一个循环中,每次先读取1KB,1KB处理后转存到其他位置,直到读完文件中的所有数据.因为Byte[]保存的是临时数据,当下次循环时临时数据就会被覆盖丢弃.Byte[]将相当于一个临时缓存,因此通常会将它命名为buffer.

 

使用流进行文件复制

 

1/ 一次性复制

 

文件复制有更简洁的方法,但是这里谈论的是流,使用流的方式进行复制.

 

回想前面的东西,首先要做的是建立起应用程序和磁盘文件的联系,建立起他们之间的管道.C#,这通过创建一个文件流的对象来完成:

Stream source = new FileStream(@"D:\headqq.bmp",FileMode.Open,FileAccess.Read);  

上面的语句创建了一个文件流对象.

构造函数的第一个参数指定了文件的路径;

第二个参数是一个枚举,指定操作系统打开文件的方式,对着这个按理来说,是打开现有文件,并从头读取,因此使用FIleMode.Open,类似的,还有FileMode.Create,FileMode.Append;

第三个参数表明了打开文件的意图,从名字就能开出,FileAccess.Read是进行读取,类似的,还有Write用于写入,ReadWrite用于读写.


第二个参数和第三个参数可以随意搭配,如果第二个参数选择Append,表示向文件尾写入数据,就是所谓的追加,而第三个参数选择Read表示只读,则会引发异常.所以说,你在搭配的时候要考虑一下,别出现某些挺让人摸不着头脑的错误,你如果说你想吃草莓味的山楂,柠檬味的葡萄你这就是有点那个啥了.

 

然后,需要将文件通过流保存到应用程序中的Byte[]数组中.由于数组的长度是固定的,在声明时就要确定,好在Stream有一个Length属性以字节为单位表示流的长度.所以可以想下面这样声明:

Stream source = new FileStream(@"D:\headqq.bmp",FileMode.Open,FileAccess.Read);  byte[] buffer = new byte[source.Length];  

对于本例中的文件流来说,可以直接访问length属性来获得文件的总字节数.但如同上面所讲的,并不是所有的流都可以访问Length属性.比如,文件位于远程服务器,通过网络流NetworkStream来访问它,这种情况下,访问Length属性总是抛出异常.

 

在声明了Byte[]数组之后,需要将文件以字节的方式读入到Byte[].这个步骤可以通过在Stream对象上调用Read()方法来完成.注意这里用到了一个很特别的词”读入”,它包含了两层意思,一个是”读”,指读取文件中的数据,一个是”入”,指将文件数据写入到Byte[]数组.如下:

int bytesRead = source.Read(buffer,0,(int)source.Length);  

Read()方法接受三个参数,第一个参数buffer,即要写入的字节数组;

第二个参数是相对于文件头的偏移量,0的意思是代表从文件的头部开始读取.

第三个参数表示读取的字节数.这里使用了一个强制类型转换,文件的字节数可以很大,比如一个ISO光盘镜像文件可以达到7GB以上,int类型无法表示这么大的数值,所以Length属性采用了long类型,如果将7GB的文件全部读取到内存中显然不现实,所以第三个参数的类型采用了int类型.因为int的最大值为2147483647,所以实际上一次最多只能读取2GB的数据.对于这个实例来说,显然可以满足.下面展示了int类型可以表示的最大字节数转换:

Console.WriteLine(Int32.MaxValue/1024/1024/1024);  

Read方法返回了读取到的字符数,通常等于Read()方法的第三个参数count.当返回0,表示已经读到了文件末尾,也就是流的终点,至此就完成了将图片文件转换为Byte[]数组的所有工作.如果愿意,可以将byte[]数组中的内容显示出来,就得到了一串数字,这串数字就代表一个图片文件:

foreach (byte b in buffer)  {      Console.Write(Convert.ToString(b,2));//二进制      Console.Write(Convert.ToString(b, 10));//十进制      Console.Write(Convert.ToString(b, 16).ToUpper());//十六进制  }  

现在需要看看逆操作了,将应用程序中的Byte[]数组保存至文件.首先需要创建文件,并且创建文件流对象.不需要像上面那样先创建一个空文件,再去写入他.可以使用FileStream的重载方法来创建新文件,同时获得流对象:

Stream target = new FileStream("C:\headqq2.bmp",FileMode.Create,FileAccess.Write);

在机器角度,文件是没有类型的,文件的后缀只是帮助操作系统决定由那个应用程序来打开,因为本示例中传输的是一个bmp图片文件,所以后缀名为bmp,实际上可以将后缀名命名为任何东西.

Stream source = new FileStream(@"D:\headqq.bmp",FileMode.Open,FileAccess.Read);  byte[] buffer = new byte[source.Length];  int bytesRead = source.Read(buffer,0,(int)source.Length);  Stream target = new FileStream(@"F:\headqq2.bmp",FileMode.Create,FileAccess.Write);  target.Write(buffer,0,buffer.Length);  target.Dispose(); 

这里为啥要换成F盘了?因为貌似win7以上C盘需要提供权限,楼主这里为了简单,就换了.

 

上面的代码就是一个复制的小小的实现,当然还有很多的不足,比如如果路径不存在啊,各种类型的错误.

 

 

2/ 循环分批复制

 

上面的代码实现的是一次性复制,但是更多见的情况是传递一个更大的文件,或者事先无法得知文件大小(Length属性抛出异常),因此也就不能创建一个尺寸正好合适的byte[]数组,此时只能分批读取和写入,每次只读取部分字节,直到文件末尾,后面的内容多采用这种形式.

这次咱们复制一个大一点的文件,一首歌曲.

int BufferSize = 10240;//10KB  Stream source = new FileStream(@"D:\MJ.mp3", FileMode.Open, FileAccess.Read);  Stream target = new FileStream(@"D:\JM.mp3", FileMode.Create, FileAccess.Write);    byte[] buffer = new byte[BufferSize];  int bytesRead;  do  {      bytesRead = source.Read(buffer,0,BufferSize);      target.Write(buffer,0,bytesRead);  } while (bytesRead>0);  source.Dispose();  target.Dispose();  

上面这段代码有三点需要注意的地方:

(1).定义了一个BufferSize变量,在声明数组和读取流时,使用BufferSize变量,而不是直接使用10240.这是为了避免出现前后不一致的情况,如果Read()方法的第三个参数(读取的字节数)超过了Byte[]数组的长度,在调用Write()时则会抛出异常.

(2)注:Read()Write()方法的第二个参数一直保持为0.这个参数是读取或写入buffer时相对于数组第一个元素的偏移量,因为本示例中要写入或读出buffer中的全部字节,因此总是从0开始.

(3)当打开文件或创建文件时,流指针默认位于文件头,当调用Read()/Write()方法后,流指针会自动向后移动相应的字节数.因此在代码中无须设置,每次调用Read()Write(),读取或写入的都是尚未处理过的字节.

 

 

流的类型体系

 

前面咱们用到了和流相关的两个类:StreamFileStream.很多人都觉得和流相关的类型很多很杂,其实是没有弄清楚这些类型之间的关系.可以将它们分为这样几类:

 

下面咱们一一介绍一下这几个流

 

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[]数组的方式变得更加丰富.

Byte[] buffer = { 5, 4, 3, 2, 1 };  MemoryStream ms = new MemoryStream(buffer);  int rtn = ms.ReadByte();  Console.WriteLine(rtn);//输出5  

 

2.装饰器流

 

装饰器流区别于基础流的特点主要有下面两点:

(1).装饰器流实现了Decorate模式,它们包含了一个Stream流基类的引用,同时继承自Stream基类.Decorate翻译成中文是装饰器,因此将它们叫做装饰器流.Decorate模式的作用是为现有类添加功能,因为装饰器是基于Stream基类的,而不是特定的FileStreamMemoryStream,因此它可以为所有的流添加功能.

(2)因为装饰器流是基于Stream基类的,所以他没有后备存储的概念,并不对应一个文件,一段内存区域或一个网络端口.它可以应用于所有的流类型.

 

装饰器流如下所示:

 

这些装饰器流并没有全部定义在System.IO命名空间下.DeflateStreamGZipStream位于System.IO.Copression,用于压缩和解压缩;CryptoStream位于System.Security.Cryptography,用于加密解密;AuthenticatedStream位于System.NET.Security,用于安全性.只有BufferedStream位于SYstem.IO,用于增强缓存.

 

3.包装器类

 

大家应该先明白关于流的最常用的方法:数据传输.当然更多的时候,读取一个文件并不是为了将它复制到另一个地方,而是读取和处理文件的内容.当文件内容从文件中经过流传递到应用程序中之后,变成了一串数字组成的Byte[]数组,那么要如何读取文件内容呢?和流相关的一组包装器类提供了这个服务,它可以方便的协助开发者处理流所包含的数据,并且不需要将流转存为Byte[]数组的形式.能明白我的意思吧?

 

看看题目,是类而不是流.说明这已经不是流类型了,但是这些类型包含了一个流的引用,提供了对流进行操作的简便方法,相当于一个包装器(Wrapper).包装器包含两组:

 

每组包装类都包含了两个类型,Reader后缀的类型用于读取,Write后缀的类型用于写入.

 

(1)StreamReaderStreamWrite

StreamReader继承自TextReader,用于将流中的数据读取为字符.StreamWrite则相反,用于将字符写入到流中.值得一提的是,虽然TextReaderTextWrite分别是StreamReaderStreamWrite的基类,但是和流一点关系都没有,它只是定义了一组通用的,读取和写入字符数据的方式..NET,还有两个类型,StringReaderStringWrite,他们也继承了TextReaderTextWrite,但是它们用于处理字符串,而不是流.

string text =      @"产品:Lumia 920      售价:2999      发售:待发";  StringReader reader = new StringReader(text);  int c = reader.Read();  Console.WriteLine((char)c);    char[] buffer = new char[8];  reader.Read(buffer,0,buffer.Length);  Console.WriteLine(string.Join("",buffer));    string line = reader.ReadLine();  Console.WriteLine(line);    string rest = reader.ReadToEnd();  Console.WriteLine(rest);    reader.Dispose();  

现在假设上面text中的字符串定义在一个文本文件中,只需要简单的将上面的StringReader换成StreamReader就可以了,甚至不需要对其余的代码做任何修改.上面的text变量声明的字符串保存在应用程序根目录下的about.text文本文件中.StringReader的声明换成下面语句.

FileStream fs = new FileStream("about.txt",FileMode.Open,FileAccess.Read);  StreamReader reader = new StreamReader(fs,Encoding.GetEncoding("GB2312"));  

 

凡是涉及文本文件的,就不可避免的遇到编码方式的问题.简单的说,编码方式定义了字节如何转换成人类刻度的字符或者文本,可以将它想象成一个字节和字符的对应关系表.如果文件创建时使用一种编码方式,比如GB2312,读取时又采取了另一种编码方式,比如UTF-8编码,那么就不能转换为正确的自负了,将会得到一堆乱码.上面代码将编码方式指定为了GB2312,正是因为在创建文件时,将其保存为了GB2312格式.关于如何查看文本格式的内容这里不多说了.

 

上例中,在创建StreamReader实例时,为它的构造函数传递了一个FileStream对象.StreamReader类还有一些重载方法,可以将这个过程变得更简洁,比如直接传递一个文件路径:

StreamReader reader = new StreamReader("about.txt");  

 

注意这里没有指定编码方式默认是使用UTF-8.所以上面的语句相当于:

StreamReader reader = new StreamReader("about.txt",Encoding.UTF8);

StreamReader reader = new StreamReader("about.txt",Encoding.UTF8);  

 

StreamWrite的使用方法也同样简单,这里只进行一个简单的演示:

StreamWriter writer = new StreamWriter("about.txt",false,Encoding.UTF8);  for (int i = 0; i < 3; i++)  {      writer.WriteLine("line"+i);  }  writer.Dispose();  

 

(2).BinaryReaderBinaryWrite

 

StreamReaderStreamWrite分别适用于读取和写入文本字符的场合,BinaryReaderBinaryWrite的适用范围更加广泛.BinaryWrite用于向流中以二进制方式写入基元类型,例如int,float,char,string;BinaryReader则用于从流中读取基元类型.他们是独立的类,不继承字TextReaderTextWrite.

 

案例如下:使用BinaryReaderBinaryWrite的使用方式

public class Product  {      public int Id { get; set; }      public string Name { set; get; }      public double Price { set; get; }        private string filePath;        public Product(string filePath)      {          this.filePath = filePath;      }      public void Save()      {          FileStream fs = new FileStream(this.filePath,FileMode.Create,FileAccess.Write);          BinaryWriter writer = new BinaryWriter(fs);          writer.Write(this.Id);          writer.Write(this.Name);          writer.Write(this.Price);          writer.Dispose();               }      public void Load()      {          FileStream fs = new FileStream(this.filePath,FileMode.Open,FileAccess.Read);          BinaryReader reader = new BinaryReader(fs);          this.Id = reader.ReadInt32();          this.Name = reader.ReadString();          this.Price = reader.ReadDouble();          reader.Dispose();      }      public override string ToString()      {          return string.Format("Id:{0} , Name:{1} , Price:{2} , ",this.Id,this.Name,this.Price);      }  }  

测试代码如下:

Product product = new Product("product.txt")  {      Id = 187,      Name="SYX",      Price=123F                                       };  product.Save();  Product newItem = new Product("product.txt");  newItem.Load();  Console.WriteLine(newItem);  

你也可以打开product.txt文件看看.可能会出现乱码的情况.

 

 

4.帮助类

 

流体系中的最后一种是帮助流,这些帮助流与流的关系其实已经不那么密切了,但是它们又能够使对文件流的操作变得简单,并且和Stream类一样,也位于System.IO命名空间下,因此这里也将它们归为流的体系中.这些帮助流有FileFileInfo.

 

File是一个静态类,提供了对文件的快速操作,比如,上例中创建一个文件时使用File会更加简单:

FileStream fs = File.Create("1.txt");  

打开文件也是一样,FIle提供了Open(String Path,FileMode mode),OpenRead()OpenWrite()几个方法,从方法名就可以看出方法的用法.除此之外,File还提供了其他一些快捷操作的方法,例如ReadAllText()以文本方式读取文件的全部内容,ReadAllByte()以字节方式读取文件全部内容.与读取项对象,还有写入的方法,例如WriteAllBytes(),WriteAllLines().如果想复制文件,只需要调用File类中的Copy(String sourceFileName,string destFileName).

 

所以说,对于文件,大家几乎用不着手动创建FileStreamStreamReader/StreamWriter,应该优先考虑File静态类中快速方法.

 

FileInfoFile是相对应的,只不过它是一个普通类,通过创建一个FileInfo的实例对象来进行类似的操作.

 

System.IO下还有其他一些类也很有用,这里就不一一介绍了,比如Path,用于处理路径;Directory类和DirectoryInfo,FIleFileInfo的关系类似,只不过它们是用来处理文件夹的.


--

The End!

再次声明,本文转自http://blog.csdn.net/shanyongxu/article/details/51039303,请点击连接查看原文,尊重楼主版权。


原创粉丝点击