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

敬请期待~

posted @ 2024-01-30 16:19  ycx-x  Views(1330)  Comments(0Edit  收藏  举报