1、流的基本概念
流是任何输入输出库的必不可少的组成部分。当你的程序需要从一个外部数据源(比如,files、other PCs或servers等)读或者写数据时,就需要用到流streams。
流是由一个应用程序或输入设备发往另一个应用程序或输出设备的有序字节序列。这些字节被一个接一个地写入和读取,并且总是按照发送的先后顺序依次到达。可用于对后备存储进行读取和写入操作,后备存储可以是多个存储媒介,比如磁盘或内存。正如存在除磁盘之外的多种后备存储一样,也存在除文件流之外的多种流(如网络、内存和管道流)。
流只允许顺序数据访问,如果对流进行随机访问,将导致读取的信息发生错误。同时,记得在使用完流之后关闭它,否则,可能冒着丢失数据,损坏文件的风险。
2、流的基本操作
一般而言,可以对流进行以下操作:creation
- Creation
创建create或打开open一个流意味着将流连接到实际的数据源。
- Reading
读取:从流中抽取数据。读取总是按照流的当前位置依次执行,它是一种阻塞操作,如果在我们试图读取数据时另一方没有发送数据,或者发送的数据尚未到达,则可能会发生延迟——几毫秒到几小时、几天或更长时间。例如,当从网络流中读取数据时,可以由于网络的原因或者另一方可能还没有开始发送数据而减慢速度。
- writing
写入:以特定的方式向流发送数据。写入操作是从流的当前位置执行的。在数据发送之前,写入可能是一个潜在的阻塞操作。例如,如果您通过网络流发送大量数据,那么当数据在网络上传输时,操作可能会被延迟。
- Positioning
寻址:在流中定位或寻址意味着移动流的当前位置。移动是根据当前位置进行的,我们可以根据当前位置、流的开始或流的结束进行定位。移动只能在支持定位的流中进行。例如,文件流通常支持定位或寻址,而网络流则不支持。
- Closing
关闭:关闭或断开一条流意味着已完成与流相关的工作,关闭流并释放被占用的资源。关闭必须在流达到其目的后尽快进行,因为用户打开的资源通常不能被其他用户使用(包括与我们的程序并行运行的同一台计算机上的其他程序)。
3、流在.NET中的实现
流在.NET中的实现就是一个Object,是用来传输数据的通道,注意,流不是数据容器(data container)。例如:FileStream传递字节数组到物理文件(比如.txt、.exe、.jpg等物理文件),NetworkStream通过socket传递字节数组。在System.IO命名空间中有一个抽象基类Stream,该类不能被实例化,它仅仅定义了其他具体实现类应具有的基本功能。
在那些具体实现了Stream基类的流中,缓冲流(buffered streams)不添加任何额外的功能,仅使用缓冲区(buffer)从其他流读取和写入数据,这大大提高了指定的I/O操作的性能。
其他一些具体实现流则添加了额外的功能来读取和写入数据。例如,有压缩/解压缩发送给它们的数据的流,以及加密/解密数据的流。这些流连接到另一个流(例如文件或网络流),并向其功能添加额外的处理。
System.IO命名空间中与流相关的类主要由Stream(.NET框架中所有流的抽象基类)、BufferedStream、FileStream、MemoryStream、GZipStream和NetworkStream。
4、流的分类
流可以根据处理的数据类型大致划分为两大类:二进制流和文本流。
4.1 二进制流
二进制流可以处理二进制(原始)数据。正因如此,使得它们具有通用性,可以使用它们从各种文件(包括图像、音乐和多媒体文件和文本文件等)中读取信息。
用于从二进制流读写的主要类有:FileStream、BinaryReader和BinaryWriter。
FileStream类为我们提供了从二进制文件(读/写一个字节和一个字节序列)读取和写入的各种方法,跳过一些字节,检查可用的字节数,当然还有关闭流的方法。我们可以通过使用参数(文件名)调用该类的构造函数来获得该类的对象。
借助于BinaryWriter类可以将基元类型(primitive types)和二进制值以特定的编码方式写入到流中。它有一个主要的方法——write(),该方法允许写任何基元数据类型——整数、字符、布尔值、数组、字符串等等。
借助于BinaryReader类可以读取使用BinaryWriter记录的基元数据类型和二进制值。它的主要方法允许我们读取一个字符、字符数组、整数、浮点数等。
4.2 文本流
文本流与二进制非常相似,但只适用于文本数据或者说字符(char)序列、字符串(string)。
在.NET中处理文本流的主要类是TextReader和TextWriter,但它们是抽象类,不能实例化,仅定义了以下几个基本功能:
- - ReadLine() -读取一行文本并返回一个字符串。
- - ReadToEnd() -读取整个流直至末尾并返回一个字符串。
- - Write() -将字符串写入流。
- - WriteLine() -将一行文本写入流中。
我们知道,.NET中的所有字符都表示成16位Unicode码值,进而所有字符串都由16位Unicode 码值构成,但是流既可以使用Unicode编码(也即UTF-16编码)也可以使用其他编码,比如:ASCII、UTF-8等。
4.3 二进制流和文本流的关系
在写入文本时,类StreamWriter负责将文本转换(即编码)为字节,然后将其记录在文件的当前位置。在此过程中,它使用在创建过程中设置的字符编码方案。在.NET中,若不显式指定一种编码方案,默认使用UTF-8。StreamReader类的工作原理类似。它在内部使用StringBuilder,当从文件中读取二进制数据时,它将接收到的字节转换为文本,然后将文本作为读取的结果发送回来。
请记住,操作系统没有“文本文件”的概念。文件总是一个字节序列,但是它是文本还是二进制取决于这些字节的解释。如果我们想将文件或流视为文本,我们必须使用文本流(StreamReader或StreamWriter)对其进行读写,但如果我们希望将其视为二进制,则必须使用二进制流(FileStream)进行读写。
请记住,文本流总是以文本行的形式读取或写入,也就是说,它们将二进制数据解释为一系列文本行,各个文本行之间用行分隔符彼此分隔。
对于不同的平台和操作系统,用来代表行分隔符的字符并不相同。对于UNIX和Linux,它是LF (0x0A),也即" ";对于Windows和DOS,它是CR + LF (0x0D + 0x0A),对于Mac OS(直到版本9),它是CR (0x0A),也即" "。从给定文件或流中读取一行文本意味着读取一个字节序列,直到读取其中一个字符CR或LF,并根据流使用的编码将这些字节转换为文本。类似地,向文本文件或流写入一行文本意味着写入文本的二进制表示(根据当前编码),然后是当前操作系统的新行(如CR + LF)的字符(或字符)。
请注意,一行被定义为一个字符序列,后跟一个换行(“ ”)、一个回车(“ ”),或者一个回车后紧接一个换行(“ ”)。但使用StreamReader的ReadLine方法读取到的返回字符串中不包含回车或换行字符,若到达输入流的末端,则返回的值为null。
图1 windows系统文本文件中的一行,后跟换行和回车
图2 StreamReader的ReadLine方法仅返回有效文本字符串,不包含换行或回车
另外,StreamWriter的WriteLine方法将根据程序运行的平台和操作系统在要写入的字符串末尾添加回车或换行字符。而Write方法则不添加。这里说个小插曲:我之前在通信公司做自动化信令文件生成的工具开发,其中就用到了StreamWriter类的WriteLine方法向最终生成.DAT格式信令文件写入数据,但是该工具是基于windows平台开发的winform应用程序,导致生成的行分隔符包括CR + LF,而后要将该信令文件拿到linux平台上使用,由于对行分隔符的定义不同,导致解析错误。折腾了好久才发现这个bug,最后的解决办法是:将待写的字符串用+号手动加上linux平台的" "换行符,然后使用Write方法写入信令文件中。
5、StreamReader和StreamWriter
首先,需要明确的是StreamReader和StreamWriter不是流stream,不从System.IO.Stream派生,而是直接从System.Object派生。它们的作用仅仅是辅助从流中读取文本或字符数据,而不是原始的字节数组。
为什么会出现这两者呢?因为Stream是一种低级的传递数据的方式,它仅能操作字节数组。没有提供处理整数或字符串的方法。这就使得Stream更具通用性,假如遇到传输文本字符串的场景,那么操作起来就不方便了。而在我们的实际开发中,经常需要传递一些基元类型(如int、float、string等),尽管基元类型是给人使用的,最终还是要转变成字节数组。
幸好,.NET提供了在本机类型和底层流接口之间进行转换的类,即:StreamWriter、StreamReader、BinaryWriter、BinaryReader。
为了使用以上这些类型,首先需要打开或创建一个流,然后将该流的实例对象传递给上述几个类型的构造函数。其中,StreamReader和StreamWriter负责将本机类型转换为它们对应的字符串表示,然后将转换后的字符串再次隐式转换为字节数组(该隐式转换即为对字符进行编码)传递给底层流,反之亦然。若处理的是文本文件,那么就使用这两个类。
图3 使用StreamWriter将int类型写入流中
例如,以上代码会首先调用Int32.ToString()方法将int类型的a转换为字符串形式"123",然后再经隐式转换为字节数组传递给底层流。
然而,若把上例的StreamWriter改为BinaryWriter,后者将会把代表32为有符号整数值123的4字节表示(0x7B,0x00,0x00,0x00)传递给底层流。若处理的是二进制文件,可使用这两个类。
6、StreamReader和BinaryReader的区别
首先,说明一点:两者都可以从二进制文件中获取数据,我们上面也提到了,对操作系统而言,没有“文本文件的概念”,数据都是字节序列。
StreamReader可以用来从文本的二进制表示中获取文本数据。而BinaryReader则可以获取任意的二进制数据。这些二进制数据中可能碰巧有一些是文本的二进制表示形式。
使用准则:如果你的全部数据均是二进制编码的文本数据,则使用StreamReader。如果你要获取的数据应是二进制形式的,可能碰巧其中有一些文本的二进制形式表示,那么应使用BinaryReader。特别是当读取图像文件或压缩文件时,应使用BinaryReader。
图4 MSDN上关于BinaryReader和BinaryWriter类的使用例子
上面是MSDN上讲述BinaryReader类的使用时举的一个例子,从上图可以看出,在用BinaryWriter类向AppSettings.dat文件写入数据时,不仅有字符串,还有float、int、Boolean类型数据。因此在读取的时候我们就不能一股脑的用StreamReader来读取,因为我们写入的时候并不是以字符的形式写入的(int和char类型的字节形式的表示长度不一样!),而应该用BinaryReader中的ReadSingle、ReadString、ReadInt32和ReadBoolean分别读取,这样才能读到正确的数据。
7、流的寻址或定位
有一些流能seek或poision,比如文件流,当我们用StreamReader读取文本文件流时,若在读取一次之后,用seek方法或position属性重新定位底层文件流,则需要调用一次StreamReader类的DiscardBufferedData方法,以使得StreamReader对象的内部buffer的地址指针和底层文件流的保持一致,这样才能达到从底层文件流的指定地址再次读取数据的目的。该方法会降低程序的性能,应尽量避免使用,仅当需要多次读取流中的相同位置的内容时才应使用。