首页 > 编程语言 >C# 文件流:Stream篇(一)

C# 文件流:Stream篇(一)

时间:2024-10-18 10:23:48浏览次数:5  
标签:文件 memoryStream Stream C# readBuffer int new memoryStreamAsync

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

敬请期待~

标签:文件,memoryStream,Stream,C#,readBuffer,int,new,memoryStreamAsync
From: https://www.cnblogs.com/sexintercourse/p/18473732

相关文章

  • DeviceNet转Profibus DP总线协议转换网关
    一,设备主要功能捷米特JM-DP-DNT网关实现DeviceNet从站设备接入到ProfibusDP网络;也可作为DeviceNet从站,将DeviceNet主站设备接入到Profibus网络。应用广泛:捷米特JM-DP-DNT广泛应用于支持DeviceNet接口的罗克罗尔,欧姆龙,基恩士PLC等主站控制器等等。DeviceNet从站转ProfibusD......
  • .netcore console 日志和配置
    前言做开发一般会写一些console程序进行调试或者小范围的处理,这里记录下console加日志和配置的过程日志日志这里选择serilog,serilog提供sink,控制台这里我们安装sink.Console和Sinke.File。一共三个nuget包SerilogSerilog.Sinks.ConsoleSerilog.Sinks.File然后代码中配......
  • CnetOS安装Tomcat
    CnetOS安装Tomcat第一步:准备环境确保你的CentOS系统是最新的:sudoyumupdate-y第二步:安装必要的软件包安装Java运行环境(JRE)或Java开发工具包(JDK),因为Tomcat需要Java来运行:sudoyuminstalljava-1.8.0-openjdk-devel-y你可以通过以下命令检查Java版本:java-versi......
  • docker-certbot-dnspod 使用 Docker 申请、续期免费证书
    项目地址https://github.com/chenlongqiang/docker-certbot-dnspod背景近期免费证书有效期从1年缩短到3个月,避免经常要上云平台手动申请,所以想找个工具可以简单的申请、续期证书。通过了解,发现Certbot工具,但官方没提供Dnspod插件,于是找了Python3的封装并打包成......
  • Linux中文件的读写过程
    文件的读取过程在Linux系统中,读取文件的过程主要由操作系统内核通过文件系统与存储设备的交互来完成。以下是文件读取过程的详细步骤:1.系统调用阶段当用户程序(如cat、less)请求读取文件时,会调用系统调用(如open()或read())来请求访问文件。这些调用会传递文件路径等参数给内......
  • 智能高效,智慧监管:EasyCVR视频汇聚平台助力煤矿构建一体化视频监控系统
    随着物联网、大数据、云计算等技术的快速发展,智慧化转型已成为煤矿行业提升生产效率、保障安全的重要途径。煤矿生产环境复杂多变,存在高温、低氧、多尘、黑暗等不利因素,给传统的人工巡检和管理方式带来了极大的挑战。EasyCVR视频汇聚平台作为智慧煤矿建设的重要组成部分,凭借其强大......
  • vue,xlsx,xlsx-style,file-saver,生成Excel并导出,cptable报错,合并单元格 样式缺失
    一,安装依赖 二,导入依赖import*asXLSXfrom'xlsx';import*asXLSX_STYLEfrom'xlsx-style'import{saveAs}from'file-saver';三,解决引入xlsx-style./cptable模块找不到问题Thisrelativemodulewasnotfound:*./cptablein./node_modules......
  • MultipartEncoder处理request请求为表单数据时
    -----------------------------7e713d354f0fa6Content-Disposition:form-data;name="username"log_username-----------------------------7e713d354f0fa6Content-Disposition:form-data;name="password"log_pwd----------------------------......
  • 过期大米被重新销往乡村学校?EasyCVR平台如何构建食品卫生安全视频监管方案
    近期,重庆市市场监管局发布的一则通报引起了社会广泛关注。通报指出,酉阳县某公司存在将过期大米重新包装并销往乡村学校的行为,这一事件再次将校园食品卫生安全问题推向了风口浪尖。面对这样的食品安全隐患,如何加强监管、确保学生饮食安全成为亟待解决的问题。而EasyCVR安防综合管......
  • CMDB平台(基础篇):CMDB的概念以及现状
    CMDB:IT界的“超级大脑”,现状却让人哭笑不得在IT界,有一个神秘而强大的存在,它被称为CMDB——资产配置管理。听起来就像是《复仇者联盟》里的超级英雄,但实际上,它更像是IT界的“超级大脑”,默默记录着每一个IT组件的“身世”和“关系网”。 那CMDB到底是什么呢?下面我们就聊一聊CM......