C# 数据流 FileStream

// Stream MS HelpManual: https://learn.microsoft.com/zh-cn/dotnet/api/system.io.stream?view=net-8.0
// FileStream 官方手册: https://learn.microsoft.com/zh-cn/dotnet/api/system.io.filestream?view=net-8.0
// StreamWriter: https://learn.microsoft.com/zh-cn/dotnet/api/system.io.streamwriter?view=net-8.0
// StreamRead: https://learn.microsoft.com/zh-cn/dotnet/api/system.io.streamreader?view=net-8.0


/* 关于FileStream的概念
 * FileSrteam 为文件提供 Stream,既支持同步读写操作,也支持异步读写操作。
 * 而Stream 是所有流的抽象基类也是FileStream的父类。 流是字节序列的抽象,例如文件、输入/输出设备、进程中通信管道或 TCP/IP 套接字。 类 Stream 及其派生类提供这些不同类型的输入和输出的通用视图,并将程序员与操作系统和基础设备的特定详细信息隔离开来。
 * 流涉及三个基本操作:
 * 1. 可以从流中读取数据。 读取是将数据从流传输到数据结构中,例如字节数组。
 * 2. 可以写入流。 写入是将数据从数据结构传输到流中。
 * 3. 流可以支持查找。 查找是指查询和修改流中的当前位置。 搜寻功能取决于流具有的后备存储类型。 例如,网络流没有当前位置的统一概念,因此通常不支持查找。
 * 
 * 要理解一点, FileStream 类处理的是流, 什么是流? 它是一种对设备抽象出来的对象, 能够读取和写入这个对象的类型仅能是byte(字节)类型.
 * 计算机的所有信息(对象)都是二进制, 而二进制是字节类型, 所以只要计算机能够访问就能够用Stream来表达;
 * FileStream 是一种线性的状态,必需要开始和结束, 就像一跟线有头和尾中间不能断.  主要就是它有一套 打开>操作>关闭 的流程,
 * 需要使用using关键字来调用和结束资源. 因为FileStream操作完之后必需关闭流, 以防止操作系统句柄到处乱放导致内存泄漏风险的发生;
 */

/* using 资源释放
 * using 除了导入名称空间和别名空间名, 还有的作用就是定义资源调用.
 * 
 * using可以算是.NET中新的语法元素,它清楚地说明一个通常比较占用资源的对象何时开始使用和何时被手动释放。
 * 当using可以被使用时,建议尽量使用using语句。至今为止,使用using语句发现它带给程序员的只有优点,而没有任何弊端。
 * 在.NET的环境中,托管的资源都将由.NET的垃圾回收机制来释放,而一些非托管的资源则需要程序员手动地将它们释放。
 * .NET提供了主动和被动两种释放非托管资源的方式,即IDisposable接口的Dispose方法和类型自己的Finalize方法。
 * 任何带有非托管资源的类型,都有必要实现IDisposable的Dispose方法,并且在使用完这些类型后需要手动地调用对象的Dispose方法来释放对象中的非托管资源。
 * 
 * using语句的作用就是提供了一个高效的调用对象Dispose方法的方式。
 * 对于任何IDisposable接口的类型,都可以使用using语句,而对于那些没有实现IDisposable接口的类型,使用using语句会导致一个编译错误。
 * 
 * using语句为实现了IDisposable的类型对象调用Dispose方法,using语句能够保证使用的对象的Dispose方法在using语句块结束时被调用,无论是否有异常被抛出。
 * C#编译器在编译时自动为using语句加上try/finally块,所以using的本质和异常捕获语句一样,但是语法更为简洁。所有using使用的对象都应该在using语句开始后再初始化,以保证所有的对象都能够被Dispose。
 *
 *经常会和 Stream 类使用在一块
 * 参考链接: https://www.cnblogs.com/dotnet261010/p/12329706.html
 */




using System.Text;
using System.Diagnostics;
using System.Xml.Linq;

namespace ConsoleApp_Stream
{
    internal class Program
    {
        static void Main(string[] args)
        {
            // FileStream_read();  // FileStream 读取文件
            // FileStream_write();  // FileStream 写入文件
            // StreamWrite_Example(); // StreamWriter 快速写入文件
            // StreamRead_Example();  // StreamReader 快速写入文件
        }


        static void FileStream_read()  // FileStream 读取文件
        {
            
            string path = "d:/1.txt";  // 指定一个文件
            FileInfo fileInfo = new FileInfo(path);  
            Console.WriteLine(fileInfo.Length);

            // 为打开文件设定合适的数据空间, 才能够完整读入
            byte[] byte_write = new byte[fileInfo.Length];   
            char[] char_write = new char[fileInfo.Length];

            // 由于打开文件经常引发错误, 必要使用try来嵌套.
            try
            {
                using (FileStream fileStream = new FileStream(path, FileMode.Open))  // 需要使用两个关键参数, 一个是指定文件, 第二个是调用文件的方式, 文件的存在或不存在会影响调用结果. 查看具体说明: https://learn.microsoft.com/zh-cn/dotnet/api/system.io.filemode?view=net-8.0
                {
                    fileStream.Seek(0, SeekOrigin.Begin);   // 流有一个指针, 指定文件的位置(内存的位置). 所以需要读取或写入之前指定指针的位置, 使用Seek方法来修改指针位置,
                                                            // 有两个参数, 一: byte(字节)类型, 从0起始, 如果使用-5则是从尾部倒数; 二: SeekOrigin指定开始计算的起始位置, 有三个枚举参数:Begin(开始) Current(最近) End(结束)    
                                                            // 若没有指定Seek方法, 默认会从0开始,使用Begin;
                    /*  Seek 方法说明:
                     //     将该流的当前位置设置为给定值。
                     //
                     // 参数:
                     //   offset:
                     //     相对于 origin 的点,从此处开始查找。
                     //
                     //   origin:
                     //     使用 System.IO.SeekOrigin 类型的值,将开始位置、结束位置或当前位置指定为 offset 的参考点。
                     //
                     // 返回结果:
                     //     流中的新位置。*/

                    fileStream.Read(byte_write,0,Convert.ToInt32(fileInfo.Length));   // 以byte格式读取文件数据存入byte_write[]中;  该方法最后一个参数 最大只能返回一个Int32位的范围的数(该数表示文件的大小, 单位b), 所以对于大文件会导致数据丢失(丢失头部分).
                    /*   Read 方法说明:
                    //     从流中读取字节块并将该数据写入给定缓冲区中。
                    //
                    // 参数:
                    //   array:
                    //     当此方法返回时,包含指定的字节数组,此数组中 offset 和 (offset + count - 1) 之间的值被从当前源中读取的字节所替换。
                    //
                    //   offset:
                    //     array 中的字节偏移量,将在此处放置读取的字节。
                    //
                    //   count:
                    //     最多读取的字节数, int类型 32位。
                    //
                    // 返回结果:
                    //     读入缓冲区中的总字节数。 如果字节数当前不可用,则总字节数可能小于所请求的字节数;如果已到达流结尾,则为零。*/

                }
            }
           catch 
            {
                Console.WriteLine("Error!");
                return;
            }

            Decoder read = Encoding.UTF8.GetDecoder();  // Decoder是System.Text命名空间的类; 很明显需要转换将byte转换为char数组, 并指定其编码方式. 
            read.GetChars(byte_write, 0, byte_write.Length, char_write, 0);  // 转换数据; 共五个参数, 前三个为转换前的 byte[]类型, 后两个为转换后的char[]类型
            /*  GetChars 方法说明:
            //     在派生类中重写时,将指定字节数组的字节序列和内部缓冲区中的任何字节解码到指定的字符数组。
            //
            // 参数:
            //   bytes:
            //     包含要解码的字节序列的字节数组。
            //
            //   byteIndex:
            //     第一个要解码的字节的索引。
            //
            //   byteCount:
            //     要解码的字节数。
            //
            //   chars:
            //     要用于包含所产生的字符集的字符数组。
            //
            //   charIndex:
            //     开始写入所产生的字符集的索引位置。
            //
            // 返回结果:
            //     写入 chars 的实际字符数。*/

            Console.WriteLine(char_write); // 打印输出
            Console.ReadKey();


            /* 总结
             * 1. [新建储存变量]
             * 该方法需要定义两个数组, byte数组类型读取数据, char数组类型存放数据;
             * 2. [创建FileStream(流)读取对象]
             * 使用FileStream创建流对象, 在该对象内指定Seek(可选), 使用Read方法读取文件并以byte[]格式存入.
             * 3. [转换数据]
             * 有了byte[]数据后需要转换该数据为char格式, 使用System.Text空间的Decoder类来转换数据, 注意,生成该对象是直接指定转换的文件格式;
             * 然后使用decoder对象GetChars方法来转换, 共五个参数, 前三个为转换前的 byte[]类型, 后两个为转换后的char[]类型;
             * 
             * 总结下来就是FileStream对流的处理过程确实比较麻烦, 但其核心就是读取文件的byte(字节)到byte[], 以便下一步的操作.
            */
        }


        static void FileStream_write()   // FileStream 写入文件
        {
            string path = "d:/2.txt";  // 指定一个文件


            // 这里可以自定义char, 或可以读取其他文件.
            string str = "紫怡偏殿,一般都是失宠的王妃居住的地方,十分偏僻,满地落叶,似乎已经很久没有人居住。";
            char[] char_write = str.ToCharArray();  // 将string 转换为 char

            Encoder encoder = Encoding.UTF8.GetEncoder();
            // byte[] 大小空间因该由转换char[]来分配大小;
            byte[]  byte_write = new byte[encoder.GetByteCount(char_write,true)]; // 因为byte_write没有设定固定范围, 所以在这里需要设定一个以char_write来指定的范围值.  在这里最后一个参数flush相当于对象缓存, 如未必要true可释放内存并清除, 相当于GC
            /* GetByteCount 方法说明:
            //     在派生类中重写时,计算对“字符”范围中的一组字符进行编码所产生的字节数。 一个参数指示计算后是否要清除编码器的内部状态。
            //
            // 参数:
            //   chars:
            //     要编码的字符范围。
            //
            //   flush:
            //     若要在计算后模拟清除解码器的内部状态,则为 true;否则为 false。
            //
            // 返回结果:
            //     通过对指定字符和内部缓冲区中的所有字符进行编码而产生的字节数。*/

            encoder.GetBytes(char_write, 0, char_write.Length, byte_write, 0, true); // 可以理解为将char[]转为byte[]类型.  在这里最后一个参数flush相当于对象缓存, 如未必要true可释放内存并清除, 相当于GC
            /* GetBytes 方法说明:
            //     在派生类中重写时,将指定字符数组中的一组字符和内部缓冲区中的任何字符编码到指定的字节数组中。 一个参数指示转换后是否清除编码器的内部状态。
            //
            // 参数:
            //   chars:
            //     包含要编码的字符集的字符数组。
            //
            //   charIndex:
            //     第一个要编码的字符的索引。
            //
            //   charCount:
            //     要编码的字符的数目。
            //
            //   bytes:
            //     要包含所产生的字节序列的字节数组。
            //
            //   byteIndex:
            //     要开始写入所产生的字节序列的索引位置。
            //
            //   flush:
            //     如果要在转换后清除编码器的内部状态,则为 true;否则为 false。
            //
            // 返回结果:
            //     写入 bytes 的实际字节数。*/

            // 由于打开文件经常引发错误, 必要使用try来嵌套.
            try
            {
                using (FileStream fileStream = new FileStream(path, FileMode.Create))  // 需要使用两个关键参数, 一个是指定文件, 第二个是调用文件的方式, 文件的存在或不存在会影响调用结果. 查看具体说明: https://learn.microsoft.com/zh-cn/dotnet/api/system.io.filemode?view=net-8.0
                {
                    fileStream.Seek(0, SeekOrigin.Begin);   // 流有一个指针, 指定文件的位置(内存的位置). 所以需要读取或写入之前指定指针的位置, 使用Seek方法来修改指针位置,
                                                            // 有两个参数, 一: byte(字节)类型, 从0起始, 如果使用-5则是从尾部倒数; 二: SeekOrigin指定开始计算的起始位置, 有三个枚举参数:Begin(开始) Current(最近) End(结束)    
                                                            // 若没有指定Seek方法, 默认会从0开始,使用Begin;
                    /*  Seek 方法说明:
                     //     将该流的当前位置设置为给定值。
                     //
                     // 参数:
                     //   offset:
                     //     相对于 origin 的点,从此处开始查找。
                     //
                     //   origin:
                     //     使用 System.IO.SeekOrigin 类型的值,将开始位置、结束位置或当前位置指定为 offset 的参考点。
                     //
                     // 返回结果:
                     //     流中的新位置。*/

                    fileStream.Write(byte_write, 0, byte_write.Length);  // 写入到流, 和Read类似, 但是最后一个参数就因该为byte[]类型的大小.
                    /*   Write 方法说明:
                    //     将字节块写入文件流。
                    //
                    // 参数:
                    //   array:
                    //     包含要写入该流的数据的缓冲区。
                    //
                    //   offset:
                    //     array 中的从零开始的字节偏移量,从此处开始将字节复制到该流。
                    //
                    //   count:
                    //     最多写入的字节数。*/

                }
            }
            catch
            {
                Console.WriteLine("Error!");
                return;
            }

            Process.Start("c:\\Windows\\System32\\notepad.exe",$" {path}");  // 打开写入的文件


            /* 总结
             * 1. [获取或新建储存字符组]
             * 如string转char, 或者获取外部文件并读取.
             * 2. [转换数据]
             * Encoder需要留意, 因为对于不确定空间大小的byte[]类型, 所以增加一个GetByteCount方法来获取char的大小空间, 
             * 其中有转换的编码器缓存会保留在系统内, 需要填入true释放内存, GetBytes也是如此;
             * GetBytes方法和GetChars方法使用方式一样,  共6个参数, 前三个为char[]对象, 后两个为byte[]对象, 最后是一个缓存清除参数.
             * 3. [写到Stream(流) ]
             * FileStream 写入文件方法和读取方法没有多大区别. 
             * 
             * 总结: 一如既往的步骤繁琐, 尤其是转换byte数据, 其方法更为繁琐, 没有一个方法直接转换的来的方便. 但也知道了处理方法的过程是怎么一回事.
             */
        }

        // 还有一些更为简单的方法
        static void StreamWrite_Example() 
        {
            // FileStream fileStream = new FileStream("d:/1.txt", FileMode.Create);
            // StreamWriter streamwriter = new StreamWriter(fileStream);
            // 相当于 ↓
            // StreamWriter writer = new StreamWriter("d:/1.txt", true);   // StreamWrite 其对象读/写是默认的;   参数2: true 在文件后继续添加字符/字符串,若无文件则创建新文件. false 删除之前的文件创建一个新文件并写入数据.          
            /* StreamWrite构造函数:
            //     用默认编码和缓冲区大小,为指定的文件初始化 System.IO.StreamWriter 类的一个新实例。 如果该文件存在,则可以将其覆盖或向其追加。 如果该文件不存在,此构造函数将创建一个新文件。
            //   参数:
            //   path:
            //     要写入的完整文件路径。
            //   append:
            //     若要追加数据到该文件中,则为 true;若要覆盖该文件,则为 false。 如果为true指定的文件不存在,该参数无效,且构造函数将创建一个新文件。*/

            try 
            {
                using (StreamWriter writer = new StreamWriter("d:/1.txt", false))  // StreamWrite 其对象读/写是默认的;   参数2: true 在文件后继续添加字符/字符串,若无文件则创建新文件. false 删除之前的文件创建一个新文件并写入数据.          
                {

                    writer.WriteLine("Hello World");
                    writer.WriteLine("我的C#入门第11天");
                    writer.Flush();   // 清除缓存区
                    writer.Close();   // 关闭StreamWriter对象流
                }
            } catch {  }



        }

        static void StreamRead_Example() 
        {
            try 
            {
                using (StreamReader streamReader = new StreamReader("D:/1.txt", true)) 
                {
                    /* // RradToEnd()快速读取文件, 但仅仅支持小文件. 对于大文件会增加读取时间而且会影响内存.                    
                        var T = streamReader.ReadToEnd();  // 对于小文件 RradToEnd() 这个方法是比较不错的方法.
                        Console.WriteLine(T);
                        streamReader.Close();
                    */

                    /* // ReadLine输出
                    var T = streamReader.ReadLine();
                    // 第一次使用while来输出我是有点疑惑的, 但是看了StreamRead.ReadLine方法后才知道, ReadLine()到达流的终点则返回一个null, 这样while循环则结束;
                    while (T != null)  
                    {
                        Console.WriteLine($"{T}");
                        T = streamReader.ReadLine();  // ReadLine()到达流的终点则返回一个null, 这样while循环则结束; 因此该方法写在while最后.
                    }
                    */

                     // Read输出
                    var A = streamReader.Read();  // Read() 只能以char的方式读取.
                    while(A != -1)
                    {
                        Console.Write(Convert.ToChar(A)); // 因为char是单字符输出, 所以需要使用write方法 连续打印, 这样不会换行打印;
                        A = streamReader.Read(); // 和readLine() 一样, 到达流的终点有一个返回值为 -1, 这样while循环则结束; 因此该方法写在while最后.
                    }
                    
                }


            } catch { Console.WriteLine("Error!"); return; }



           
        }

        // 其他方法
        static void Else() 
        {
            // 还有另一种方式, 使用File类的ReadLine,或者ReadAllLine, REadAllText 方法, 该方法可以读取内容多的文件
            foreach (var R in File.ReadAllText("D:/1.txt"))
            {
                Console.Write(R);  // 由于ReadAllText是以char字符输出, 所以使用write不换行打印
            }

            // 创建一个新文件(文件存在则覆盖),在其中写入指定的字节数组,然后关闭该文件。
            string[] strings = { "123", "456", "7890" };
            File.WriteAllLines("d:/2.txt", strings);


            // 快速将字符串转换为byte
            string strsum="";
            foreach (var R in File.ReadAllLines("D:/1.txt")) { strsum += R; } // 将遍历的string 赋值给strsum变量
            byte[] bytes = new UTF8Encoding(true).GetBytes(strsum); // 初始化 System.Text.UTF8Encoding 类的新实例。 Bool参数指定是否提供一个 Unicode 字节顺序标记。
            // 之后就可以对使用Stream类方法来对bytes操作了

        }


    }
}

 

posted @ 2024-03-24 08:31  edolf  阅读(237)  评论(1编辑  收藏  举报