C# 文件流:Stream篇(一)
前话:
本文系列本着备忘的目的进行归纳,Stream系列原文链接:C# 温故而知新:Stream篇(—) - 逆时针の风 - 博客园 (cnblogs.com) 望各位看官到原作者处学习。
后几篇不作注释,还请见谅
--------------------------------------------------------------------------------------------分割线------------------------------------------------------------------------------------------------------
什么是Stream?
提供字节序列的一般视图
那什么是字节序列呢?
字节对象都被存储为连续的字节序列,字节按照一定的顺序进行排序,组成了字节序列
马上进入正题,C#中 Stream 是如何使用的
Stream 类是一个抽象类,无法直接如下使用创建实例
Stream stream = new Stream();
因此我们自定义一个流继承 Stream ,查看哪些属性必须重写或者自定义
public class StreamEx : Stream { public override bool CanRead => throw new NotImplementedException(); public override bool CanSeek => throw new NotImplementedException(); public override bool CanWrite => throw new NotImplementedException(); public override long Length => throw new NotImplementedException(); public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } public override void Flush() { throw new NotImplementedException(); } public override int Read(byte[] buffer, int offset, int count) { throw new NotImplementedException(); } public override long Seek(long offset, SeekOrigin origin) { throw new NotImplementedException(); } public override void SetLength(long value) { throw new NotImplementedException(); } public override void Write(byte[] buffer, int offset, int count) { throw new NotImplementedException(); } }
可看出,系统会自动帮助我们实现 Stream 抽象属性和方法
1. CanRead:只读属性,判断该流是否支持读取
2. CanSeek:只读属性,判断该流是否支持查找功能
3. CanWrite:只读属性,判断该流是否支持写入
4. Length:表示流长度(以字节为单位)
*5. Position属性:(重要)
从字面意思看,Position属性表示流中的当前位置。但是当 Stream 对象被缓存后,导致 Position 属性在流中无法正确找到对应的位置,其实解决这个问题很简单,我们每次在使用流前,将 Stream.Position 设置为 0 就可以了,但是这不能从根本上解决,最好的方法就是用 Using 语句将流对象包裹起来,用完后关闭回收即可
*6. void Flush():
当我们使用流写文件时,数据流会先进入到缓冲区中,而不会立刻写入文件,当执行这个方法后,缓冲区的数据流会立即注入基础流
MSDN中描述为:使用此方法将所有信息从基础缓冲区移动到其目标或清除缓冲区,或者同时执行这两种操作。根据对象的状态,可能需要修改流内的位置
当使用 StreamWriter 或者StreamReader 类时,不要刷新 Stream 基对象,而应使用该类的 Flush 或者 Close 方法,此方法确保首先将该数据刷新至基础流,然后再将其写入文件
*7. abstract int Read(byte[] buffer, int offset, int count)
这个方法包含3个关键参数:缓冲字节数据,字节偏移量,读取的字节数。每次读取一个字节后会返回一个缓冲区中的总字节数
第一个参数(byte[] buffer):这个数组相当于一个空盒子,read() 方法每次读取流中的一个字节将其放进这个空盒子里,全部读完后便可使用 buffer 字节数组了
第二个参数(int offset):表示位移偏移量,告诉我们从流中的哪个位置(偏移量)开始读取
第三个参数(int count):读取多少个字节数
返回值:总共读取多少字节数
*8. abstract long Seek(long offset, SeekOrigin origin)
不知是否还记得 Position 属性不?其实 Seek 方法就是重新设定流中的一个位置,在说明 offset 参数作用之前,大家可先了解下 SeekOrigin 这个枚举:
如果 offset 为负,则要求新位置位于 origin 指定的位置之前,其间隔相差 offset 指定的字节数
如果 offset 为零(0),则要求新位置位于由 origin 指定的位置处
如果 offset 为正,则要求新位置位于 origin 指定的位置之后,其间隔相差 offset 指定的字节数
Stream. Seek(-3,Origin.End); //表示在流末端往前数第3个位置 Stream. Seek(0,Origin.Begin); //表示在流的开头位置 Stream. Seek(3,Orig`in.Current); //表示在流的当前位置往后数第三个位置
查找之后会返回一个流中的一个新位置
9. void SetLength(long value)
设置当前流的长度
参数(value):所需的当前流的长度(以字节表示)
*10. abstract void Write(byte[] buffer, int offset, int count)
这个方法包含3个关键参数:缓冲字节数据,字节偏移量,读取的字节数。
第一个参数(byte[] buffer):这个数组在使用时就已经有了许多 byte 类型
第二个参数(int offset):表示位移偏移量,告诉我们从流中的哪个位置(偏移量)开始写入
第三个参数(int count):写入多少个字节数
*11. virtual void Close()
关闭流并释放资源,在实际操作中,如果不用 using 语句的话,别忘了使用完流之后将其关闭
这个方法非常重要,使用完当前流别忘记关闭流!
为了更明确Stream的属性和方法,请看示例
static void Main(string[] args) { try { byte[] readBuffer = null; char[] readCharBuffer = null; string messageString = "Stream Practice!"; string newMessageString = string.Empty; using (MemoryStream memoryStream = new MemoryStream()) { Console.WriteLine($"初始字符串:{messageString}"); //如果该流允许写入 if (memoryStream.CanWrite) { //首先尝试将字符串 messageString 写入流中 //通过 Encoding 实现 string -> byte[] 的转换 byte[] buffer = Encoding.Default.GetBytes(messageString); //我们从该数组的第一个位置开始写,长度为10,写完之后 memoryStream中便有了数据 //比较难以理解的是,数据是什么时候写入到流中的,在冗长的项目代码里面,都会有这个问题 memoryStream.Write(buffer, 0, 10); Console.WriteLine($"现在 Stream.Position 在第{memoryStream.Position + 1}位置"); //从刚才结束的位置(当前位置)往后移3位 long newPositionInStream = memoryStream.CanSeek ? memoryStream.Seek(3, SeekOrigin.Current) : 0; Console.WriteLine($"重新定位后 Stream.Position 在第{newPositionInStream + 1}位置"); if (newPositionInStream < buffer.Length) { //将从新位置一直写到 buffer 的末尾,此时需注意,memoryStream 已经写入了10个数据“Stream Pra” memoryStream.Write(buffer, (int)newPositionInStream, buffer.Length - (int)newPositionInStream); } //写完后将 memoryStream 的 Position 属性设置为0,开始读取流中的数据 memoryStream.Position = 0; //设置一个空盒子来接收流中的数据,长度由 memoryStream 的长度决定 readBuffer = new byte[memoryStream.Length]; //设置 memoryStream 总的读取数量 //注意,这时候流已经把数据读到了 readBuffer 中 int count = memoryStream.CanRead ? memoryStream.Read(readBuffer, 0, readBuffer.Length) : 0; //由于我们刚开始使用加密的 Encoding 方式,所以我们需要解密,将 readBuffer 中的数据转化成 char 数组,随后才能重新拼接成 string //首先,我们先将从流中都回来的数据 readBuffer 转化成相应的 char 数组 int charCount = Encoding.Default.GetCharCount(readBuffer, 0, count); //通过 char 数量,设定一个新的 readCharBuffer 数组 readCharBuffer = new char[charCount]; //Encoding 类的强悍之处就是不仅包含加密的方法,甚至将解密者都能创建出来(GetDecoder()),解密者便会将 readCharBuffer 填充 //通过 GetChars 方法,把 readBuffer 中 byte 数据逐个转化成 char ,并且按一致顺序填充到 readCharBuffer 中 Encoding.Default.GetDecoder().GetChars(readBuffer, 0, count, readCharBuffer, 0); for (int i = 0; i < readCharBuffer.Length; i++) { newMessageString += readCharBuffer[i]; } Console.WriteLine($"读取的新字符串为:{newMessageString}"); } } Console.ReadLine(); } catch (Exception ex) { throw new Exception(ex.ToString()); } }
显示结果:
特别注意:memoryStream.Position 这个属性,在复杂的程序中,流对象的操作也会很复杂,一定要将 memoryStream.Position 设置在所需要的正确位置
如上:将
memoryStream.Position = 0 改成 memoryStream.Position = 3
运行程序得出的结果是:
其次,using 语句结束前,会自动销毁 memoryStream 对象,相当于 memoryStream.Close()。
接下来学习下关于流中怎么实现异步操作
在 Stream 基类中有几个关键方法,他们能够很好的实现异步的读写
//异步读取 public virtual IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
//结束异步读取 public virtual int EndRead(IAsyncResult asyncResult) //异步写入 public virtual IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) //结束异步写入 public virtual void EndWrite(IAsyncResult asyncResult)
大家很容易就会发现,异步写入和异步读取两个方法实现的 IAsyncResult 接口,结束异步读取和写入方法中也顺应带上的 IAsyncResult 参数,其实使用并不复杂,特别需要注意的是:
每次调用 Begin 方法时,都必须调用一次相对应的 End 方法
static void Main(string[] args) { try { byte[] readBuffer_1 = null; char[] readCharBuffer_1 = null; string newMessageString_1 = string.Empty; using (MemoryStream memoryStreamAsync = new MemoryStream()) { string messageString = "Stream Practice!"; Console.WriteLine($"初始字符串:{messageString}"); if (memoryStreamAsync.CanWrite) { //通过 Encoding 实现 string -> byte[] 的转换 byte[] bufferAsync = Encoding.Default.GetBytes(messageString); //使用异步方法将前10位字符写入,“Stream Pra” memoryStreamAsync.BeginWrite(bufferAsync, 0, 10, new AsyncCallback(x => { memoryStreamAsync.EndWrite(x); }), memoryStreamAsync); Task.Delay(1000).Wait();//保证数据完成写入操作 Console.WriteLine($"现在 Stream.Position 在第{memoryStreamAsync.Position + 1}位置"); //从刚才结束的位置(当前位置)往后移3位 var newPositionStreamAsync = memoryStreamAsync.CanSeek ? memoryStreamAsync.Seek(3, SeekOrigin.Current) : 0; Console.WriteLine($"重新定位后 Stream.Position 在第{newPositionStreamAsync + 1}位置"); //将从新位置一直写到 bufferAsync 的末尾,此时需注意,memoryStreamAsync 已经写入了10个数据“Stream Pra” memoryStreamAsync.BeginWrite(bufferAsync, (int)newPositionStreamAsync, bufferAsync.Length - (int)newPositionStreamAsync, new AsyncCallback(x => { if(newPositionStreamAsync > bufferAsync.Length) memoryStreamAsync.EndWrite(x); }), memoryStreamAsync); Task.Delay(1000).Wait();//保证数据完成写入操作 //写完后将 memoryStreamAsync 的 Position 属性设置为0,开始读取流中的数据 memoryStreamAsync.Position = 0; //设置一个空盒子来接收流中的数据,长度由 memoryStreamAsync 的长度决定 readBuffer_1 = new byte[memoryStreamAsync.Length]; //设置 memoryStreamAsync 总的读取数量 //注意,这时候流已经把数据读到了 readBuffer_1 中 var rc = memoryStreamAsync.CanRead ? memoryStreamAsync.ReadAsync(readBuffer_1, 0, readBuffer_1.Length) : null; int charCountAsync = Encoding.Default.GetCharCount(readBuffer_1, 0, rc.Result); readCharBuffer_1 = new char[charCountAsync]; Encoding.Default.GetDecoder().GetChars(readBuffer_1, 0, charCountAsync, readCharBuffer_1, 0); for(int i = 0; i < readCharBuffer_1.Length; i++) { newMessageString_1 += readCharBuffer_1[i]; } Console.WriteLine($"读取的新字符串为:{newMessageString_1}"); //异步读取 EnginRead() 无法执行 //var rc1 = memoryStreamAsync.CanRead?memoryStreamAsync.BeginRead(readBuffer_1,0,readBuffer_1.Length,x => //{ // int countAsync = memoryStreamAsync.EndRead(x); // if (countAsync > 0) // { // int charCountAsync_1 = Encoding.Default.GetCharCount(readBuffer_1, 0, countAsync); // readCharBuffer_1 = new char[charCountAsync]; // Encoding.Default.GetDecoder().GetChars(readBuffer_1, 0, countAsync, readCharBuffer_1, 0); // for (int i = 0; i < readCharBuffer_1.Length; i++) // { // newMessageString_1 += readCharBuffer_1[i]; // } // Console.WriteLine($"读取的新字符串为:{newMessageString_1}"); // } //}, memoryStreamAsync) : null; } } Console.ReadLine(); } catch (Exception ex) { throw new Exception(ex.ToString()); } }
此处异步读取数据使用的是 public Task<int> ReadAsync(byte[] buffer, int offset, int count) 方法,注释部分无法执行读取操作(暂时不知道原因)
本章总结:
本章介绍了流的基本概念和C#中关于流的基类 Stream 所包含的一些重要的属性和方法,主要是一些属性和方法的细节和我们操作时必须注意的事项
遗留问题:
本章遗留问题主要是异步读取数据方法 BeginRead() 无法执行,该问题暂时无法解答,各位读友可帮忙解惑下
下一章将会介绍操作流类的工具:StreamWriter 和 StreamReader
敬请期待~