(8)基础强化:内存流,压缩流,序列化,资料管理器,编码,File类,文件流,文本流

    
    
    
一、内存流


    1、为什么要有内存流?
        
        答:内存流(MemoryStream)是一个特殊的流,它将数据存储在内存中而不是磁盘或网络。
        使用内存流的主要原因使用场景包括:
        
        内存操作:
            内存流允许我们在内存中直接读取和写入数据,而无需涉及磁盘或网络的I/O操作。
        这对于快速的内存数据操作非常方便。
        
        缓存:
            将数据存储在内存中可以提高访问速度。内存流可以用作临时缓冲区,例如在处理
        大量数据时,可以通过内存流暂存数据,然后按需读取或写入。
        
        数据序列化和反序列化:
            内存流常用于对象序列化和反序列化,尤其是在将对象存储到内存中或进行内存间
        通信时。可以使用内存流将对象转换为字节数组并在需要时进行反序列化。
        
        压缩和解压缩:
            内存流与压缩算法(如GZipStream或DeflateStream)一起使用,可以在内存中进行
        数据压缩和解压缩,而无需使用临时文件或网络传输。
        
        因此内存流可用的场景比如:
        
        图片处理:
            在处理图片时,可以将图片数据读入内存流中,然后进行操作,例如调整大小、裁
        剪、加滤镜等。操作完成后,可以将结果数据写回内存流或保存到磁盘。
        
        文件加密:
            内存流可以用于读取和写入加密文件。可以使用内存流读取加密文件内容,然后对
        其进行解密并在内存中处理,最后可以将处理结果写回内存流或保存为解密后的文件。
        
        数据压缩:
            使用内存流和压缩流(如GZipStream)可以将数据压缩到内存中,然后将压缩后的
        结果存储在内存流中,以便进一步处理或传输。
        
        总结:内存流是在内存中进行数据处理的一种方便方式。它的主要使用场景包括内存操
        作、缓存、数据序列化和反序列化,以及压缩和解压缩等。通过使用内存流,我们可以
        避免频繁的磁盘读写或网络传输,提高数据处理的效率和性能。
    
    
    2、内存流的理解
        
        内存流(MemoryStream)是一种流的实现,它将数据存储在内存中而不是磁盘或网络中。
        它提供了一种方便的方式来处理内存中的数据,就像处理文件流一样。
        
        内存流可以使用字节数组作为内部存储区域,而不需要实际的文件或网络连接。这使得
        内存流非常适合于对小量数据进行临时存储、处理和传输,而无需涉及磁盘 I/O 或网络
        操作的复杂性。
        
        内存流在处理小文件时具有以下优点:
        
        高速操作:
            内存流将数据存储在内存中,无需进行磁盘或网络的I/O操作。相比于文件操作,
            内存流可以大大提高读取和写入小文件的速度。
        
        简化代码:
            使用内存流可以简化代码逻辑,减少对临时文件的处理。无需关心文件路径、创建
            临时文件或删除文件等繁琐操作,使代码更简洁。
            
        较小的资源消耗:
            相对于处理大文件时需要使用大量磁盘空间或网络带宽的情况,处理小文件时使用
            内存流所需的内存资源较小,可以更有效地利用系统资源。
            
        灵活性:
            内存流允许直接在内存中读取和写入数据,可快速进行数据处理和转换。它提供了
            对数据的灵活访问,可以在内存中执行各种操作,如查询、修改、合并等。
            
        注意:由于内存流将数据存储在内存中,因此适用的文件大小是有限的。当处理大文件
            时,可能会对系统资源造成负担,甚至引发内存溢出。对于大文件的处理,通常需
            要采用分批读取或使用其他处理方式。
            
        结论:内存流在处理小文件时具有高速操作、简化代码、较小的资源消耗和灵活性等优
            势。它对于需要在内存中快速读取、写入和处理小文件的场景非常适用。但需要注
            意,在处理大文件时应权衡系统资源消耗,并采取相应处理策略。
        
    
    3、内存流的示例,说明了如何将字符串数据写入内存流并从内存流中读取数据:

        private static void Main(string[] args)
        {
            string content = "Hello, this is a test string.";

            // 将字符串写入内存流
            using (MemoryStream memoryStream = new MemoryStream())
            {
                byte[] contentBytes = System.Text.Encoding.UTF8.GetBytes(content);
                memoryStream.Write(contentBytes, 0, contentBytes.Length);

                memoryStream.Position = 0;// 重置内存流位置,以便读取数据

                // 从内存流读取数据
                byte[] buffer = new byte[memoryStream.Length];
                memoryStream.Read(buffer, 0, buffer.Length);

                string readContent = System.Text.Encoding.UTF8.GetString(buffer);
                Console.WriteLine("Read content from memory stream: " + readContent);
            }
            Console.ReadKey();
        }


        上面使用内存流(MemoryStream)将字符串数据写入内存,并从内存流中读取相同的数据。
        
        首先创建了一个内存流,并将字符串内容转换为字节数组,使用写操作将字节数组写入
        内存流。然后将内存流的位置重置(Position 属性设置为 0),以便能够从内存流中
        读取数据。接下来创建了一个字节数组作为缓冲区,使用读操作从内存流中读取数据。
        最后,将读取到的字节数组转换为字符串,并显示。
        
        内存流非常适合在内存中临时存储和操作数据,特别是当涉及到对数据进行 CRUD 操
        作时。与文件流不同,内存流的好处是避免了对磁盘 I/O 的频繁访问,从而提高了
        性能。
        
        注意:内存流中的数据是存储在内存中的,所以对于较大的数据量,需要确保在内存
            消耗方面有足够的考虑。此外,由于内存流的生命周期受到 using 块的限制,
            因此一旦在 using 块之外使用内存流,可能会导致内存泄漏和潜在的资源问题。
            
        备注:crud是指在做计算处理时的增加(Create)、检索(Retrieve)、更新(Update)和
            删除(Delete)几个单词的首字母简写。crud主要被用在描述软件系统中数据库或
            者持久层的基本操作功能。
        
        
    4、问:byte与Byte有什么区别?
        
        答:在C#中,byte和Byte实际上是相同的类型,只是一个是关键字(byte),而另一个是
        对应的系统定义的结构(Byte)。
        
        byte是C#的关键字,它表示无符号8位整数的数据类型。它的取值范围是从0到255。
        byte通常用于表示字节数据,例如在处理图像、文件等方面。
        
        Byte是System命名空间下的结构,它提供了与byte类型相关的静态方法和属性。
        
        结论:byte和Byte在功能上是相同的,它们都用于表示无符号的8位整数数据类型。
        区别在于byte是C#关键字,而Byte是对应的系统定义的结构。在大多数情况下,可以
        使用它们进行相同的操作和赋值。

        byte[] blob = new byte[] { 0x41, 0x42, 0x43, 0x44 }; //Blob数据,含四个字节的二进制数据
        File.WriteAllBytes(@"E:\1.txt", blob);

        Byte[] readBytes = File.ReadAllBytes(@"E:\1.txt");//Byte同byte
        foreach (byte item in readBytes)
        {
            Console.WriteLine(item.ToString("X2"));
        }


        
    
    5、内存流场景使用举例:
        
        当涉及到需要在内存中进行数据操作和临时存储时,内存流是一个非常有用的工具。
        
        (1)图片处理:
        
            在图像处理过程中,可以使用内存流加载图像数据。例如,可以使用内存流从文件
            或网络加载图像数据,然后进行图像操作(如裁剪、旋转、调整大小等),最后将
            结果保存回内存流或者导出为新的图像文件。

        string scr = @"E:\1.png";
        string des = @"E:\2.png";
        // 从文件加载图像数据至内存流
        using (FileStream fileStream = new FileStream(scr, FileMode.Open))
        {
            using (MemoryStream memoryStream = new MemoryStream())
            {
                fileStream.CopyTo(memoryStream);

                // 在内存中对图像进行处理
                using (Image image = Image.FromStream(memoryStream))
                {
                    // 执行一些图像操作
                    image.RotateFlip(RotateFlipType.Rotate90FlipNone);
                    image.Save(des);
                }
            }
        }


        
        上面在控制台操作时会有错误提示。原因请参考:
        https://blog.csdn.net/dzweather/article/details/131454121?spm=1001.2014.3001.5501
        建议:上面程序在窗体程序中练习。
        
        
        (2)文件加密和解密:
        
            内存流在加密和解密文件时也非常有用。例,可以将加密的文件加载到内存流中,
            然后对其进行解密操作,最后将解密后的数据保存回内存流或导出为原始文件。

        // 从文件加载加密文件数据至内存流
        using (FileStream fileStream = new FileStream("encrypted_file.dat", FileMode.Open))
        {
            using (MemoryStream memoryStream = new MemoryStream())
            {
                fileStream.CopyTo(memoryStream);

                // 解密数据
                byte[] decryptedData;
                using (Aes aes = Aes.Create())
                {
                    // 设置解密密钥和其他参数...

                    // 解密数据
                    using (CryptoStream cryptoStream = new CryptoStream(memoryStream, aes.CreateDecryptor(), CryptoStreamMode.Read))
                    {
                        using (MemoryStream decryptedStream = new MemoryStream())
                        {
                            cryptoStream.CopyTo(decryptedStream);
                            decryptedData = decryptedStream.ToArray();
                        }
                    }
                }
                // 处理解密后的数据...
            }
        }


        
        
        (3)数据压缩和解压缩:
        
            内存流与压缩流(如GZipStream)一起使用,可以在内存中进行数据压缩和解压
            缩,而无需使用临时文件或网络传输。

        // 压缩数据至内存流
        string data = "Some data to compress";
        byte[] compressedData;
        using (MemoryStream memoryStream = new MemoryStream())
        {
            using (GZipStream gzipStream = new GZipStream(memoryStream, CompressionMode.Compress))
            {
                using (StreamWriter writer = new StreamWriter(gzipStream))
                {
                    writer.Write(data);
                }
            }
            compressedData = memoryStream.ToArray();
        }


        上面压缩后在compressData中,也可以保存下来:

        using (FileStream fileStream = new FileStream("compressedData.gz", FileMode.Create))
        {
            fileStream.Write(compressedData, 0, compressedData.Length);
        }
        
        
        // 解压缩数据
        string decompressedData;
        using (MemoryStream memoryStream = new MemoryStream(compressedData))
        {
            using (GZipStream gzipStream = new GZipStream(memoryStream, CompressionMode.Decompress))
            {
                using (StreamReader reader = new StreamReader(gzipStream))
                {
                    decompressedData = reader.ReadToEnd();
                }
            }
        }


二、文件压缩


    1、压缩流
        
        是指用于对数据进行压缩和解压缩的数据流。它提供了一种方便的方式来处理文
        件、网络传输或内存中的压缩数据。
        
        C# 提供了以下两种主要的压缩流:
        (1)GZipStream:GZipStream 是用于处理 GZIP 压缩格式的流。它能够将数据以
            GZIP 格式进行压缩,并能够解压缩已经压缩的数据。GZipStream 基于 Deflate 
            算法。
        
        (2)DeflateStream:DeflateStream 是一个通用的压缩流,支持 Deflate 压缩格
            式。DeflateStream 可以对数据进行压缩和解压缩操作。它也是 GZipStream 
            的基础。
            
        使用压缩流的好处是能够减小数据的大小,从而节省存储空间和减少传输的数据量。
        压缩流在以下情况下有很大的应用:
            文件压缩:将文件压缩以节省存储空间或在网络上传输。你可以使用压缩流读
        取源文件,然后将压缩的字节写入目标文件。
            网络通信:在网络通信中,压缩流可以用于将数据压缩,以减少网络带宽的使
        用量。发送方可以使用压缩流将数据进行压缩,接收方则可以使用相应的解压缩流
        进行解压缩。
            内存压缩:在某些情况下,你可能需要在内存中处理大量数据。压缩流可以将
        数据压缩,以节省内存消耗。
    
    
    2、GZipStream的原理是什么?
    
        它使用 Deflate 算法对数据进行压缩和解压缩。GZipStream 的原理如下:
        
        压缩:
        (1)GZipStream 接收原始数据作为输入。
        (2)在压缩过程中,GZipStream 使用 Deflate 算法对数据进行压缩。Deflate 算法
            是一种基于 LZ77 算法的无损压缩算法,它通过查找和替换重复的数据块来减少
            数据的大小。
        (3)GZipStream 将压缩的数据进行分块,并在每个块中添加头部和校验和。
        (4)最后,GZipStream 生成了一个包含压缩数据的 GZIP 文件或数据流。
        
        解压缩:
        (1)GZipStream 接收压缩数据作为输入。
        (2)在解压缩过程中,GZipStream 解析 GZIP 文件头部,并验证校验和。
        (3)GZipStream 解压缩每个压缩块的数据,使用 Deflate 算法进行解压缩操作,还原
            为原始数据块。
        (4)最后,GZipStream 生成原始数据流作为输出。
        
        总结:GZipStream 使用 Deflate 算法对数据进行压缩和解压缩。压缩过程中,数据
        被拆分为多个块,每个块都会被压缩和附加头部和校验和。解压缩过程则是将头部和
        校验和解析后,对每个压缩块进行解压缩操作,恢复为原始数据。这个过程是有损耗
        的,因为从原始数据中的重复块创建了一个压缩流。GZipStream 提供了一种方便的
        方式来处理 GZIP 压缩格式,减小数据大小并节省存储空间。
        
    
    3、GZipStream主要用于哪些场景?
        
        答 :除了文本文件、图片和视频,GZipStream 在许多其他场景下也可以使用。以下
        是一些常见的使用场景:
        
        压缩和解压缩文件:
            GZipStream 可以用于压缩和解压缩任意文件,包括二进制文件、文档文件、日志
            文件等。
            
        网络数据传输:
            GZipStream 可以用于在网络上压缩传输数据。通过在数据传输前压缩,可以减少
            数据的传输时间和带宽消耗。
        
        缓存数据压缩:
            GZipStream 可以用于在内存中缓存数据时进行压缩。例如,在内存缓存中存储大
        量数据时,使用 GZipStream 可以减少内存的消耗。
        
        数据持久化:
            GZipStream 可以用于将数据压缩后存储到磁盘或数据库中。这在需要节省存储空
        间的场景中很有用。        关于内存的字串与图片,理论上可以使用 GZipStream 进行压缩和解压缩操作。例如,
        可以将字符串数据进行压缩后存储在内存中的字节数组中,或者将图片数据进行压缩
        后传输或存储。这种做法可以减小内存消耗或网络带宽,并且在需要时可以快速解压
        缩回原始数据。
        
        
    4、GZipStream可以的场景很多,1.图片;2.文本文件;3.电影;4.字符串;
        
        下面以文件为例,操作步骤为:
        1>压缩:
            1.创建读取流File.OpenRead()
            2.创建写入流File.OpenWrite();
            3.创建压缩流new GZipStream();将写入流作为参数与。
            4.每次通过读取流读取一部分数据,通过压缩流写入。
        2>解压
            1.创建读取流: File.OpenRead()
            2.创建压缩流:new GZipStream();将读取流作为参数
            3.创建写入流File.OpenWrite();
            4.每次通过压缩流读取数据,通过写入流写入数据
            

        private static void Main(string[] args)
        {
            //文本压缩与解压
            string sourceFile = @"E:\1.txt";
            string destinationFile = @"E:\111.txt";
            Compress(sourceFile, destinationFile);

            sourceFile = @"E:\111.txt";
            destinationFile = @"E:\2.txt";
            UnCompress(sourceFile, destinationFile);
        }

        private static void Compress(string s, string d)//压缩
        {
            //1.创建读取文本文件的流
            using (FileStream fsRead = File.OpenRead(s))
            {
                //2.创建写入文本文件的流
                using (FileStream fsWrite = File.OpenWrite(d))
                {
                    //3.创建压缩流
                    using (GZipStream zipStream = new GZipStream(fsWrite, CompressionMode.Compress))
                    {
                        //4.每次读取1024byte
                        byte[] byts = new byte[1024];
                        int len = 0;
                        while ((len = fsRead.Read(byts, 0, byts.Length)) > 0)
                        {
                            zipStream.Write(byts, 0, len);//通过压缩流写入文件
                        }
                    }
                }
            }
            Console.WriteLine("压缩完成!");
        }

        private static void UnCompress(string s, string d)//解压
        {
            //1.读源文件流
            using (FileStream fsRead = File.OpenRead(s))
            {
                //2.解压流
                using (GZipStream gzipStream = new GZipStream(fsRead, CompressionMode.Decompress))
                {
                    //3.写入流
                    using (FileStream fsWrite = File.OpenWrite(d))
                    {
                        byte[] byts = new byte[1024];
                        int len = 0;
                        //4.循环读后写
                        while ((len = gzipStream.Read(byts, 0, byts.Length)) > 0)
                        {
                            fsWrite.Write(byts, 0, len);
                        }
                    }
                }
            }
            Console.WriteLine("解压缩完成!");
        }


    
    
    5、结合内存流,快速操作小文件。
        
        下面:使用 GZipStream 压缩和解压缩内存中的字串与图片数据的示例:

        private static void Main(string[] args)
        {
            string text = "This is a sample string.";
            byte[] imageData = File.ReadAllBytes(@"E:\1.png");
        
            // 压缩字符串数据
            byte[] compressedBytes = CompressData(Encoding.Default.GetBytes(text));
            Console.WriteLine("Compressed text: " + Convert.ToBase64String(compressedBytes));
        
            // 解压缩字符串数据
            string decompressedText = Encoding.Default.GetString(DecompressData(compressedBytes));
            Console.WriteLine("Decompressed text: " + decompressedText);
        
            // 压缩图片数据
            byte[] compressedImage = CompressData(imageData);
            Console.WriteLine("Compressed image size: " + compressedImage.Length + " bytes");
        
            // 解压缩图片数据
            byte[] decompressedImage = DecompressData(compressedImage);
            File.WriteAllBytes(@"E:\2.png", decompressedImage);
        
            Console.ReadKey();
        }

        public static byte[] CompressData(byte[] data)
        {
            using (MemoryStream memoryStream = new MemoryStream())
            {
                using (GZipStream gzipStream = new GZipStream(memoryStream, CompressionMode.Compress))
                {
                    gzipStream.Write(data, 0, data.Length);
                }
                return memoryStream.ToArray();
            }
        }

        public static byte[] DecompressData(byte[] compressedData)
        {
            using (MemoryStream memoryStream = new MemoryStream(compressedData))
            {
                using (GZipStream gzipStream = new GZipStream(memoryStream, CompressionMode.Decompress))
                {
                    using (MemoryStream decompressedStream = new MemoryStream())
                    {
                        gzipStream.CopyTo(decompressedStream);
                        return decompressedStream.ToArray();
                    }
                }
            }
        }


        先使用 CompressData 方法对字符串数据和图片数据进行压缩,并将压缩结果打印或
        保存。再使用 DecompressData 方法对压缩后的数据进行解压缩,并将结果打印或保存。
        
        注意:为了方便展示压缩结果,使用 Base64 编码将字节数组转换为字符串。在实际
            使用时,可以根据需求选择适当的数据表示方法。
            


三、序列化(二进制序列化)


    1、序列化通俗理解:
        
        答:序列化就是把原数据按照一定的格式、规则,重新组织,形成有序的形式。
            反序列化就是按照上面的格式规则,反过还原原来数据的过程。
            
        比如:我们嘴说的话,是语音。按照文字的方式、格式、规则,把它记录下来,形成
        文字,这个过程相当于序列化。
            当把这个文字,又重新用嘴说出来的语音,这个过程就是反序列化。
    
    2、问:序列化的种类有哪些?
        
        答:序列化是将对象的状态转换为可存储或传输的格式的过程,以便稍后能够重新创
        建该对象。通过序列化,我们可以将对象转换为字节流或其他可用于存储、传输或持
        久化的形式。序列化使得对象可以在不同的应用程序、平台或网络环境中进行交换和
        共享。
        
        常见的序列化方式有:
        
        (1)二进制序列化:
            通过将对象转换为二进制格式,将对象的状态保存到字节流中.可用BinaryFormatter
            来实现二进制序列化。这种方式序列化后的数据通常紧凑,但对于人眼来说是不可
            读的。
            
        (2)XML序列化:
            将对象的状态转换为可以存储在XML格式中的形式。在C#中可用XmlSerializer或
            DataContractSerializer来实现XML序列化。XML序列化后的数据是可读的,可以
            被人类读取和理解,但相对于二进制序列化,通常会占用更多的存储空间。
            
        (3)JSON序列化:
            将对象的状态转换为JSON(JavaScript Object Notation)格式的字符串。可用
            JsonSerializer或DataContractJsonSerializer来实现JSON序列化。JSON序列化
            通常在Web应用程序和Web服务中使用,因为大多数现代的Web API都使用JSON作
            为数据的交换格式。
            
        除此外,还有其他自定义的序列化方式,如Protobuf(Protocol Buffers)、
        MessagePack等。这些自定义的序列化方式通常可以提供更高的性能和更小的序列化
        数据大小。
        
        注意:反序列化是将序列化的数据转换回对象的过程。反序列化的方式应该与序列化
            的方式相匹配,以确保能够正确地还原对象的状态。
    
    
    3、形象例子
    
        (1)下面把一个对象JSON序列化:

        internal class Program
        {
            private static void Main(string[] args)
            {
                Person p = new Person() { Name = "杨中科", Age = 35, Email = "yzk@itcast.com" };
                JavaScriptSerializer jsSer = new JavaScriptSerializer();
                string msg = jsSer.Serialize(p);//序列化
                Console.WriteLine(msg);//a

                Person p1 = jsSer.Deserialize<Person>(msg);//反序列化
                Console.WriteLine(p1.Name);//b

                Console.ReadKey();
            }
        }

        internal class Person
        {
            public string Name { get; set; }
            public int Age { get; set; }
            public string Email { get; set; }
        }


        a处显示:{"Name":"杨中科","Age":35,"Email":"yzk@itcast.com"}  各属性出现顺序
            与Person定义顺序有关。
        b处反序列后,显示杨中科。
        
        上面是可以用肉眼看到的序列化(JSON).
        
        (2)下面再将Person进行XML序列化。

        internal class Program
        {
            private static void Main(string[] args)
            {
                Person p = new Person() { Name = "杨中科", Age = 35, Email = "yzk@itcast.com" };
                XmlSerializer xmlSer = new XmlSerializer(typeof(Person));//a
                using (FileStream fs = new FileStream(@"E:\1.xml", FileMode.Create))
                {
                    xmlSer.Serialize(fs, p);
                }
                string s = File.ReadAllText(@"E:\1.xml");
                Console.WriteLine(s);

                using (StreamReader sr = new StreamReader(@"E:\1.xml"))
                {
                    Person p2 = (Person)xmlSer.Deserialize(sr);
                    Console.WriteLine(p2.Name);
                }

                Console.ReadKey();
            }
        }

        public class Person//只能是public,否则上面a处报错
        {
            public string Name { get; set; }
            public int Age { get; set; }
            public string Email { get; set; }

            public void Say()
            {
                Console.WriteLine("Hello");
            }
        }


        上面p序列化后存储在xml,然后再反序列回到p2。
        
        注意:1.序列化后,只是把对象的存储格式改变了,对象实际存储内容并没有改变。
            2.序列化只序列化数据。(比如,字段,属性,属性也生成字段)
            
            对于方面并不存储,如上例的Say()并不存储。
            
        在通常情况下,序列化只关注对象的状态,即字段的值。方法不是对象状态的一部
        分,并且可以通过类型(类)来调用。
    
    
    4、对象序列化(二进制序列化)
    
        二进制序列化就是把对象变成流的过程,即把对象变成byte[].
        这样就可将Person对象序列化后保存到磁盘上,要操作磁盘文件所以需要使用文件流。
        
        二进制序列化并不采用特定的编码方式,它将对象的内部表示形式直接转换为二进制
        数据流。在进行二进制序列化时,C# 使用了一种称为“二进制格式”的自定义格式,
        它不同于常见的文本编码格式(如UTF-8或UTF-16),而是直接将对象中的字段和属
        性以二进制形式进行存储。这样可以更高效地表示对象的内部结构,但编码规则并没
        有公开的标准。
        
        二进制序列化,需用BinaryFormatter进行操作。
        

        internal class Program
        {
            private static void Main(string[] args)
            {
                Person p = new Person() { Name = "杨中科", Age = 35, Email = "yzk@itcast.com" };
                BinaryFormatter bf = new BinaryFormatter();
                using (FileStream fs = new FileStream(@"E:\1.txt", FileMode.Create))//a
                {//文件名后缀名与txt无关
                    bf.Serialize(fs, p);//c
                }

                using (FileStream fs = new FileStream(@"E:\1.txt", FileMode.Open))
                {
                    Person p3 = (Person)bf.Deserialize(fs);
                    Console.WriteLine(p3.Name);
                }

                Console.ReadKey();
            }
        }
        [Serializable]
        public class Person//b
        {
            public string Name { get; set; }
            public int Age { get; set; }
            public string Email { get; set; }

            public void Say()
            {
                Console.WriteLine("Hello");
            }
        }


        a处文件的后缀名无关紧要。
        b处前面必须加上可序列化[Serializable]否则c处报错。
        
        被序列化的类必须注明[Serializable],它告诉编译器和运行时环境,这个类是可序
        列化的,并且可以被BinaryFormatter、XmlSerializer等序列化器使用。只有添加了
        这个注明,才能确保类的实例可以被正确地序列化和反序列化。
        
        同时这个类还必须满足:
            (1)类必须是公共的(public)或内部的(internal)。
            (2)类必须有无参数的构造函数),以确保对象可以被正确地实例化。
            (3)类的字段或属性必须是可序列化的。
            
        注意:标记为可序列化,并不意味着所有的成员都会被序列化。例如,静态字段、事件
            和方法通常不会被序列化,因为它们不是对象的状态的一部分。
            
        可序列化的类要求必须具有无参构造函数:
            因为反序列化时,会使用无参构造函数创建类的实例,然后通过将序列化数据的
            值赋给对象的字段或属性来还原对象的状态。如果类没有无参构造函数,反序列
            化过程就无法正确创建对象实例,并将状态还原到该实例中。
            
            注意:如果类中没有明确提供无参构造函数,编译器会为类生成一个默认的无参构
            造函数。但若已有带参数的构造函数,编译器将不再生成默认的无参构造函数,
            这时我们需要显式地提供一个无参构造函数。
            
            
        上面类改成下面:

        [Serializable]
        public class Animal//d
        {
        }

        [Serializable]
        public class Person : Animal//b
        {
            private Car Bechi;//f
            public string Name { get; set; }
            public int Age { get; set; }
            public string Email { get; set; }

            public void Say()
            {
                Console.WriteLine("Hello");
            }

            public void SayChina()
            {
                Car car = new Car();//g
            }
        }

        [Serializable]
        public class Car//e
        {
        }


        b处继承后,则其父类d处前面也须加上[Serializable]。若f处字段的类,则该类也
        应被序列化(e处),但在g处无需序列化,因为它是方法内,与状态无关。
        
        
        二进制序列化的注意点:
        (1)被序列化的对象的类型必须标记为可序列经;
        (2)被序列化的类的所有父类也必须标记为可序列化;
        (3)要求被序列化的对象的类型中的所有字段(属性)的类型也必须标记为可序列化。
        
        
        提示:
            [Serializable]特性主要用于二进制序列化,在需要使用BinaryFormatter等二进
            制序列化器时,可以标记类为[Serializable]以确保其可序列化。对于其他序列化
            方式,如JSON序列化和自定义序列化逻辑,则不需要依赖[Serializable]特性。
        
        
    5、问:为什么只能对公共字段成员进行序列化?
        
        答:(1)只有字段表示状态,方法表明行为。序列化只需要状态,故仅对成员有效。
            (2)只对公共字段有效。
                因为私有字段只能在类内部访问,无法从外部直接访问。因此外部的序列化器
                无法直接访问私有字段。
                
            如果在初始化对象时没有为公共字段赋初值,那么这个字段也会被序列化,但它的
            值将是 null(对于引用类型)或默认值(对于值类型)。

        Person p = new Person() { Name = "杨中科", Age = 35};
        [Serializable]
        public class Person//b
        {
            public string ID;
            public string Name { get; set; }
            public int Age { get; set; }
        }


            上面公共字段ID没赋值,但也会被序列化,值为null。
        
        C# 中已知的基础类型、值类型和引用类型都是可以进行序列化的,可以使用内置的序
        列化器如 BinaryFormatter、XmlSerializer、JsonSerializer 等来进行序列化和反
        序列化操作。
        
        需要注意序列化的类型需要满足一些要求,例如类需要标记为 [Serializable]。
        这个可能通过按F12查看定义,它的上面都会标明[Serializable]
        
        
    6、NonSerialized 不可序列化的。
        
        但有时候,我们可能希望某个字段不参与序列化,例如敏感数据、密码或暂时无需保
        存的计算字段。因为这些数据序列化存储或网络传输时,可能泄密。
        
        这时只须在这个字段前面添加[NonSerializable]
        
        注意:[NonSerialized]只能在二进制序列化中使用。

        [Serializable]
        public class MyClass
        {
            public int AField;
            [NonSerialized]
            public string BData;
            // ...

            // ...
        }   

     
        上面序列化时,AField是可以参与序列化的,但BData是不能参与到序列化中。
        
        XML 序列化使用 XmlIgnore 特性来指示字段不应该被序列化:

        [Serializable]
        public class MyClass
        {
            public int nonSerializedField;
            [XmlIgnore]
            public string sensitiveData;//不参与序列化
            // ...

            // ...
        }   

     
        
        JSON 序列化使用 JsonIgnore 特性来指示字段不应该被序列化:

        [Serializable]
        public class MyClass
        {
            public int nonSerializedField;
            [JsonIgnore]
            public string sensitiveData;//不参与序列化
            // ...

            // ...
        }   

     
        
        
    7、问:私有字段是无法序列化?
        
        答:错误 
            私有字段默认情况下不会被序列化,因为序列化器无法直接访问私有字段。如果
            需要序列化私有字段,可以使用序列化特性 [DataMember] 标记字段,或者实现
            自定义的序列化逻辑。需要注意序列化私有字段可能会破坏封装性,需要慎重考
            虑是否真的需要序列化私有字段。
        
        私有字段序列的方法有:
        
        (1)使用序列化特性:
            可以使用 [Serializable] 特性标记类,并使用 [DataMember] 特性标记私有字
            段,以明确告诉序列化器要序列化这些私有字段。

            [Serializable]
            public class MyClass
            {
                [DataMember]
                private int privateField;
                // ...
            }     

   
        
        (2)自定义序列化:
        
            可以在类中实现自定义的序列化逻辑,手动控制对私有字段的序列化和反序列化
            过程。这可以通过实现 ISerializable 接口来实现。

            public class MyClass : ISerializable
            {
                private int privateField;
                // ...

                public void GetObjectData(SerializationInfo info, StreamingContext context)
                {
                    info.AddValue("privateField", privateField); // 手动添加私有字段到序列化信息中
                    // ...
                }

                // ...
            } 

       
        
        注意:序列化私有字段可能会破坏封装性,使私有字段的具体值暴露给外部,因此需
            要慎重考虑是否真的需要序列化私有字段。
    
    
    8、BinaryFormatter类有两个方法
        
        void Serialize(Stream stream, object graph) 
                        对象graph序列化到stream中
        object Deserialize(Stream stream)  
                        将对象从stream中反序列化,返回值为反序列化得到的对象
        
        什么是序列化器?
            C# 中的序列化器是一种用于将对象转换为字节流或其他可传输/可存储的格式的
            工具。序列化器负责将对象的状态进行编码,以便可以在不同的环境中进行传
            输、存储或还原为对象。
            
            序列化器有BinaryFormatter、XmlSerializer、JsonSerializer。除此外,还可
            以使用其他第三方库或自定义的序列化器来满足不同的需求。
            
            注意:选择合适的序列化器取决于具体的需求和场景。例如,如果需要高性能和
                紧凑的序列化格式,可以选择 BinaryFormatter;如果需要与其他平台或语
                言进行交互,可以选择 XmlSerializer 或 JsonSerializer。
        
        二进制序列化,必须相配。序列化时用的什么类型,反序列化还原将是什么类型,一
        把钥匙配一把锁。序列化是MyClass类型,反序列化还原时也将是MyClass类型。
        
        另外,二进制序列化,都会创建序列化器BinaryFormatter.
        
        根据上面4大项,若仅有下面代码,反序列化将报错。

        private static void Main(string[] args)
        {
            BinaryFormatter bf = new BinaryFormatter();
            using (FileStream fs = new FileStream(@"E:\1.txt", FileMode.Open))
            {
                object p3 = bf.Deserialize(fs);
            }

            Console.ReadKey();
        }


        因为序列化时只是将类型的一些公共字段进行序列化存储。还原时,还需重新创建一个
        实例化对象,还需要私有字段,方法等,因此必须要原来的类型Person存在,且有一个
        无参构造函数,才能还原。
        
        可以用两种方法消除上面错误,一种是把原来的类型Person重新写一次。另一种在本项
        目引用前面序列化时所在的项目(变相地引用原来的类型)
        
        强调:反序列化时,如果存在父类的继承关系,需要提供父类的类型信息,以便正确地
        进行反序列化并还原对象。通过在反序列化过程中进行类型转换,可以成功地还原包含
        继承关系的对象结构。同样,若有接口一样得提供。通俗地说,想用反序列化这把钥
        匙,必须原模原样的还原原来的类型这把锁。使得钥匙与锁配套。
        
        注意:反序列化时,原来的类型上面也必须标明[Serializable]
        
        作业:结合前面的内存流,把Person序列化到内在流中,并反序列化。

        internal class Program
        {
            private static void Main(string[] args)
            {
                Person p = new Person() { Name = "魔法师", Age = 999 };
                BinaryFormatter bf = new BinaryFormatter();
                using (MemoryStream ms = new MemoryStream())
                {
                    bf.Serialize(ms, p);//a

                    ms.Position = 0;//b
                    Person p1 = bf.Deserialize(ms) as Person;
                    Console.WriteLine(p1.Name);
                }
                Console.ReadKey();
            }
        }

        [Serializable]
        public class Person
        {
            public string Name { get; set; }
            public int Age { get; set; }
        }


        注意:上面b处必须重新重新指定位置为0.因为在使用内存流进行序列化后,内存流的 
        Position 属性将指向序列化数据内存流的末尾位置。而在进行反序列化时,需要将读
        取位置重新设置为序列化数据的起始位置,以便从头开始读取数据。
        
        若要查看内存流序列化的内容,可以在a处后添加下面内容:

        byte[] b = ms.ToArray();
        Console.WriteLine(Encoding.Default.GetString(b));


        因为序列化是二进制,我们把内存流转数据后得到b,再通过编码显示这个字符,内容就
        和存储在文件中一样(也有难认的乱码)。
        
        注意:ms.ToArray()不会改变内存流的位置,该方法只是将内存流数据复制到一个新的
            字节数组中。原先是末尾,之后仍然在内存流的末尾。
            
            
        练习:将几个int、字符串添加到ArrayList中,并序列化到文件中,再反序列化回来

        private static void Main(string[] args)
        {
            ArrayList alist = new ArrayList() { 1, 2, 3, "a", "b", "c" };
            BinaryFormatter bf = new BinaryFormatter();
            using (FileStream fs = new FileStream(@"E:\1.txt", FileMode.Create))
            {
                bf.Serialize(fs, alist);
            }

            using (FileStream fs = new FileStream(@"E:\1.txt", FileMode.Open))
            {
                ArrayList alist1 = bf.Deserialize(fs) as ArrayList;
                Console.WriteLine(alist1[1]);
            }
            Console.ReadKey();
        }


        
        
    9、问:不建议使用自动属性,因为每次生成的字段都可能不一样,影响反序列化?
    
        答:使用自动属性是一种常见且推荐的做法。自动属性提供了一种简洁的语法来定
        义属性,编译器会自动生成幕后字段。
        
        在反序列化过程中,关键是保持属性的名称和类型一致,这样反序列化器才能正确
        地还原对象。使用自动属性不会影响反序列化的过程。
        
        
    10、问:反序列化时只需提供父类,无需提供子类?
    
        答:这个不完全正确。
            在C#中,反序列化一个对象不需要原类的所有父类,只需要有对应的公共字段
            或属性即可。当你使用反序列化方法时,可以提供需要反序列化的类型,并将
            数据与该类型匹配,而不必担心父类或子类。
            

            public class MyBaseClass
            {
                public string BaseProperty { get; set; }
            }

            [Serializable]
            public class MyDerivedClass : MyBaseClass
            {
                public string DerivedProperty { get; set; }
            }

            class Program
            {
                static void Main()
                {
                    // 创建对象并进行序列化
                    var obj = new MyDerivedClass()
                    {
                        BaseProperty = "Base",
                        DerivedProperty = "Derived"
                    };

                    XmlSerializer serializer = new XmlSerializer(typeof(MyDerivedClass));
                    using (StreamWriter writer = new StreamWriter("data.xml"))
                    {
                        serializer.Serialize(writer, obj);
                    }

                    // 进行反序列化
                    using (StreamReader reader = new StreamReader("data.xml"))
                    {
                        var deserializedObj = (MyBaseClass)serializer.Deserialize(reader);//a
                        Console.WriteLine(deserializedObj.BaseProperty); // 输出: Base
                    }
                }
            }


            上面反序列化时,是按基类MyBaseClass反序列化的,所以只能访问基类属性,
            不能访问子类属性.
            
            如果想要能够访问子类的属性,可以将反序列化的对象类型设置为

            MyDerivedClass 而不是 MyBaseClass
            using (StreamReader reader = new StreamReader("data.xml"))
            {
                var deserializedObj = (MyDerivedClass)serializer.Deserialize(reader);
                Console.WriteLine(deserializedObj.BaseProperty);        // 输出: Base
                Console.WriteLine(deserializedObj.DerivedProperty);    // 输出: Derived
            }    

        
            这样,就可以访问并输出 MyDerivedClass 类中定义的属性 DerivedProperty。
            
            在给定的场景中,源类型是MyDerivedClass,反序列化的目标类型是MyBaseClass。
            由于 MyBaseClass 是 MyDerivedClass 的基类,因此,在反序列化过程中,
            会将序列化的数据填充到 MyBaseClass 类型的对象中。
            
            需要注意的是,由于反序列化的目标类型是 MyBaseClass,因此只能访问和
            使用 MyBaseClass 类型中定义的公共字段或属性。子类独有的字段或属性
            将无法在反序列化后的对象中访问。如果要访问子类特有的字段或属性,应
            该将反序列化的目标类型设置为子类的类型。
            
            故:反序列化一个对象不需要原类的所有父类,只需要具有对应的公共字段
                或属性的目标类型即可。
            
            
    11、作业:制作日志或笔记记录,并序列化保存在磁盘上,可添加修改。
    

 

        private void Form1_Load(object sender, EventArgs e)
        {
            DeSerializeData();
        }

        private void button1_Click(object sender, EventArgs e)//保存
        {
            string key = textBox1.Text.Trim();
            if (key != null)
            {
                if (dic.ContainsKey(key))
                {
                    dic[key] = textBox2.Text;
                }
                else
                {
                    dic.Add(key, textBox2.Text);
                }
                SerializeData();

                listBox1.Items.Clear();
                for (int i = 0; i < dic.Count; i++)
                {
                    listBox1.Items.Add(dic.Keys.ElementAt(i));
                }

                textBox1.Text = "";
                textBox2.Text = "";
            }
        }

        private void SerializeData()
        {
            BinaryFormatter bf = new BinaryFormatter();
            using (FileStream fs = new FileStream(@"E:\1.txt", FileMode.Create))
            {
                bf.Serialize(fs, dic);
            }
        }

        private void DeSerializeData()
        {
            if (File.Exists(@"E:\1.txt"))
            {
                BinaryFormatter bf = new BinaryFormatter();
                using (FileStream fs = new FileStream(@"E:\1.txt", FileMode.Open))
                {
                    dic = bf.Deserialize(fs) as Dictionary<string, string>;
                }
                if (dic.Count > 0)//有记录
                {
                    listBox1.Items.Clear();

                    for (int i = 0; i < dic.Count; i++)
                    {
                        listBox1.Items.Add(dic.Keys.ElementAt(i));
                    }
                }
            }
        }

        private void button2_Click(object sender, EventArgs e)
        {
            DialogResult result = MessageBox.Show("是否退出程序?", "退出", MessageBoxButtons.YesNo);
            if (result == DialogResult.Yes)
            {
                Application.Exit();
            }
        }

        private void listBox1_DoubleClick(object sender, EventArgs e)
        {
            if (listBox1.SelectedIndex != -1)
            {
                textBox1.Text = dic.Keys.ElementAt(listBox1.SelectedIndex);
                textBox2.Text = dic.Values.ElementAt(listBox1.SelectedIndex);
            }
        }   

     
        
        注意:1.dic公共变量,要先初始化。使用赋值时,注意两种赋值方式。有时没有key值时
                用dic[key]=value会异常。
            2.textbox.text=""与textbox.clear()是有区别的。
            两者均清空文本。
            但clear()是TextBox 控件的一个成员方法,用于清空文本框的内容。除了清空文本
            内容之外,Clear() 方法还会执行其他操作,包括清除选择区域、重置滚动位置以
            及触发 TextChanged 事件。这个方法适用于需要更完整的清空操作或需要在清空文
            本框时执行特定逻辑的场景。例如,重新设置文本框,或者在文本框内容变化时执
            行额外的操作。
            

            textBox1.Clear();// 清空文本框的内容
            
            textBox1.ReadOnly = false;// 重置相关的属性
            textBox1.BackColor = Color.White;


            
            上面清空并重置相关属性。这样可以确保文本框不仅仅是清空了内容,还恢复到了
            初始的状态。
            
            若仅是清空文本,还是用""办法,因为clear()更费资源。
        
        


四、资料管理器


    1、STAthread 单线程单元模式single thread apartment thread
        
        进程相当于一个小城镇。线程相当于这个城镇里的居民。
        STA(单线程套间)相当于居民房,是私有的。
        MTA(多线程套间)相当于旅馆,是公用的。
        Com对象相当于居民房或旅馆里的物品。
        
        于是,一个小城镇(进程)里可以有很多很多的(居民)线程,这个城镇(进程)只有一
        间旅馆(MTA),但可以有很多很多的居民房(STA)。
        
        只有居民(线程)进入了房间(居民房或旅馆,STA或MTA)以后才能使用该房间里的物
        品(COM对象)。
        
        居民房(STA)里的物品(COM对象)只能供这间房子的主人(创建该STA的线程)使用,
        其它居民(线程)不能访问。
        
        同样,只有入住到旅馆(MTA)里的居民(线程,可以有多个)才可以访问到旅馆(MTA)
        里的物品(com对象),但因为是公用的,所以要合理的分配(同步)才能不会产生混乱。
        
        
        [STAThread]是C#中的一个属性,用于指示应用程序的主线程需要以单线程单元 
        (STA) 模式运行。
        
        在多线程编程中,STA模式表示单线程单元模式,它要求应用程序的主线程是一
        个单线程模式,并且能够处理与COM (Component Object Model) 交互相关的操
        作。COM是一种用于组件间通信的技术,常见于使用Windows API、ActiveX控件、
        COM组件等场景。
        
        具体来说,[STAThread]特性通常用于将应用程序的主线程标记为运行在STA模式
        下。这是因为在STA模式中,必须确保应用程序在执行与COM交互的操作时,不会
        发生线程冲突或死锁。
        
        MTA 是 Multiple Thread Apartment 的缩写,指的是多线程单元模式。在 MTA 模
        式下,多个线程可以同时与 COM (Component Object Model) 对象进行交互。
        
        在 MTA 模式中,多个线程可以共享同一个单线程单元 (apartment) 中的 COM 对
        象。这些线程可以并行执行,并且可以同时调用同一个 COM 对象的方法。
        
        与 STA 模式不同,MTA 模式下的线程没有自己的消息队列。它们直接调用 COM 对
        象的方法,而不需要通过消息泵来分发和处理消息。
        
        MTA 模式的使用场景包括:
            开发多线程应用程序,其中多个线程需要同时与 COM 对象进行交互。
            在使用COM组件的第三方库或框架中,调用了要求在MTA模式下运行的COM对象。
            
        注意:由于多个线程可以同时访问和修改共享的资源,因此在 MTA 模式下需要注
            意线程同步和资源共享的问题,以避免竞争条件和数据一致性问题。

        在某些情况下,可以通过将 COM 对象标记为 “Both” 来支持 STA 和 MTA 模式的
        同时使用,以便兼容不同的线程模型。
        

        namespace Forms
        {
            internal static class Program
            {
                /// <summary>
                /// 应用程序的主入口点。
                /// </summary>
                [STAThread]
                static void Main()
                {
                    Application.EnableVisualStyles();
                    Application.SetCompatibleTextRenderingDefault(false);
                    Application.Run(new Form1());
                }
            }
        }


        注意:
            [STAThread] 属性是针对整个应用程序的,并不是针对单个窗体。只需在主入口
            方法中添加一次即可。
            
            通常,可以将 [STAThread] 属性添加到应用程序的主入口方法 Main() 或者在 
            Program.cs 文件中的 Main() 方法中。(项目中双击Program.cs显示Main())
        
        MTA如同上面一样进标注:

        static class Program
        {
            [MTAThread]   // 添加 [MTAThread] 属性
            static void Main()
            {
                Application.EnableVisualStyles();
                Application.SetCompatibleTextRenderingDefault(false);
                Application.Run(new MainForm());  // 这里的 MainForm 是你应用程序的主窗体类
            }
        }        


        
        STA与MTA中的A一样吗?
            两者"A"皆指"Apartment",但功能和工作方式上是不同的。
            
            在 STA 模式中,一个单线程单元 (apartment) 中只能有一个线程与
        COM (Component Object Model) 对象进行交互。该线程负责处理该单元中的所有
        操作,包括创建、调用和销毁 COM 对象。STA 模式下的线程使用消息泵来接收
        和处理操作系统的消息,并确保 COM 对象的线程同步和协同工作。
        
            与之相反,MTA 模式下允许多个线程同时与 COM 对象进行交互。多个线程可以
        共享同一个单线程单元 (apartment) 中的 COM 对象,它们可以并行执行,而无需
        通过消息泵来分发和处理消息。
        
            因此,STA 和 MTA 的主要区别在于线程数量和线程同步的机制。STA 模式只
        允许一个线程与 COM 对象交互,通过消息泵进行线程同步;而 MTA 模式允许多个
        线程同时与 COM 对象交互,并且在多线程环境下需要额外考虑线程同步和资源共
        享的问题。
        
        消息泵:
            消息泵用于描述在图形用户界面 (GUI) 应用程序中的消息处理机制。
            
            可以将消息泵比喻成一个类似于水泵的装置。水泵从水源中抽取水,并将其分
        发到需要的地方。同样,消息泵从操作系统中获取消息,并将其分发给应用程序中
        的各个部分。
        
            在一个 GUI 应用程序中,操作系统会向应用程序发送各种消息,例如鼠标点
        击、键盘输入、窗口移动等等。这些消息需要被应用程序捕获和处理,以便做出相
        应的响应或执行相应的操作。
        
            消息泵的作用就是循环地从操作系统获取消息,并将其分发给适当的消息处理
        程序来处理。消息处理程序可以是应用程序中的窗口过程、事件处理函数或其他回
        调函数。消息泵会按照消息的顺序逐个分发消息,确保每个消息得到处理。
        
            简而言之,消息泵就像是一个消息传递的中转站,它负责从操作系统接收消
        息,并将其传递给应用程序中的相应部分进行处理。
        
            消息泵将产生的消息按照一定的顺序分发给不同的程序或组件,并且是单向
        的、依次进行的。就像水流一样,消息从操作系统传递给应用程序的消息泵,然
        后按照一定的顺序被依次取出,通过调用对应的消息处理程序来处理。每个消息
        按顺序经过消息泵,不会交错或跳跃。
        
            这种顺序性确保了消息的正确处理顺序,避免了消息之间的混乱或冲突。类
        似于水流中的流动一样,消息泵按照消息的产生顺序对消息进行排队和分发,以
        确保每个消息都被及时处理。
        
    
    2、SplitContainer 拆分容器
        
        SplitContainer用于创建分隔面板或分割窗格。SplitContainer控件可以将窗体分
        割为两个可调整大小的面板,用户可以通过拖动分隔条来调整面板的大小。
        
        分隔面板(Panel)
            SplitContainer 控件中的两个面板被称为分隔面板。通常分别被称为 Panel1 
            和 Panel2。你可以在这两个面板中添加其他控件或容器来创建你的界面布局。
        分割条(Splitter)
            分割条是一个控件,用于调整两个面板的大小。它位于两个分隔面板之间,当
            用户通过鼠标拖动分割条时,可以改变两个面板的大小。
            
        即SplitContainer 是一个包含两个分隔面板和一个分割条的容器控件。分隔面板用
        于容纳其他控件,而分割条用于调整两个面板的大小。
    
        常用属性:
            Orientation:获取或设置 SplitContainer 的拆分方向,可以是水平或垂直。
            Panel1 和 Panel2:获取 SplitContainer 的两个分隔面板。
            SplitterDistance:获取或设置分隔条的位置(以像素为单位),用于调整两
                                个面板的大小。
            SplitterWidth: 确定拆分器的厚度(以像素为单位)。
            IsSplitterFixed:获取或设置一个值,指示是否禁止用户使用鼠标拖动分隔条
                                来调整面板大小。
            FixedPanel:获取或设置一个值,指示在调整大小时哪个面板保持固定大小。
            
        常用方法:
            SplitterMoving: 拆分器移动时发生。
            SplitterMoved:当分隔条移动后发生的事件。通常用于在分隔条移动后执行一
                                些自定义操作。
            ResetSplitterDistance:将分隔条的位置重置为默认位置。
    

        splitContainer1.Orientation = Orientation.Horizontal;
        Panel panel1=splitContainer1.Panel1;
        Panel panel2 = splitContainer1.Panel2;
        splitContainer1.SplitterDistance = 200;
        splitContainer1.IsSplitterFixed = true;
        splitContainer1.FixedPanel = FixedPanel.Panel1;


        
        问:上面isSplitterFixed与FixedPanel有什么区别?
        答:FixedPanel用于指定在(如窗体)调整大小时,哪个面板将保持固定大小。
        当设置为FixedPanel.Panel1 时,Panel1 面板将保持固定大小,Panel2变化。
        当设置为 FixedPanel.Panel2 时,Panel2 面板将保持固定大小。
        当设置为 FixedPanel.None 时,无面板保持固定大小,两个面板同时调整大小。
        
        IsSplitterFixed 属性:
            用于指示用户是否可以通过鼠标拖动分隔条来调整面板大小。
            默认值为 false,允许用户调整分隔条位置。
            当设置为 true 时,禁止用户通过拖动分隔条来调整面板大小。
        
        若要禁止用户通过鼠标拖动改变左侧面板的大小,可用下面方法:
            (1)将 SplitContainer 的 IsSplitterFixed 属性设置为 true,以禁止分隔
                条的移动。例如:splitContainer1.IsSplitterFixed = true;
            (2)在 SplitContainer 的 SplitterMoving 事件中取消事件,阻止分隔条的
                移动。

            private void splitContainer1_SplitterMoving(object sender, SplitterCancelEventArgs e)
            {
                e.Cancel = true;
            }


    
        问:SplitContainer可以嵌套放置吗?
        答:可以。
            可以将一个 SplitContainer 控件放置在另一个 SplitContainer 的一个或两个
            面板中,以创建更复杂的布局。这样可以实现多层次的分隔面板,使你的应用程
            序的界面更加灵活。
            
            例:可以在主SplitContainer的一个面板中放置一个垂直分隔的次级SplitContainer,
            然后在次级 SplitContainer 的一个面板中再放置一个水平分隔的第三级 
            SplitContainer。这样就形成了一个嵌套的 SplitContainer 结构。
    
    
    3、图书管理器

 

        private void button1_Click(object sender, EventArgs e)
        {
            string path = @"E:\Test";
            LoadDirectory(path, treeView1.Nodes);
        }

        private void LoadDirectory(string path, TreeNodeCollection nodes)
        {
            string[] dirs = Directory.GetDirectories(path);
            foreach (string dir in dirs)
            {
                TreeNode node = nodes.Add(Path.GetFileName(dir));
                LoadDirectory(dir, node.Nodes);
            }

            foreach (string item in Directory.GetFiles(path, "*.txt"))
            {
                TreeNode node = nodes.Add(Path.GetFileName(item));
                node.Tag = item;
            }
        }

        private void treeView1_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e)
        {
            if (e.Node.Tag != null)
            {
                //编码处
                textBox1.Text = File.ReadAllText(e.Node.Tag.ToString());//a
            }
        }


        
        技巧:
            由于SplitContainer占满整个窗体,而TreeView也占满整个Panel1,因此在设置
            属性时不方便,不能准确地选择某一控件。有两种方法解决:
            (1)右击窗体重叠位置,里面可以选择你要确定的控件;
            (2)在属性面板中,上面的对象中选择对应的控件。
        
        事件中sender 是一个表示触发事件的对象实例的参数。它通常用于事件处理程序中,
        以帮助确定事件来自于哪个控件或对象。
        
        在TreeView的NodeMouseDoubleClick事件中,sender参数指示引发事件的TreeView
        控件的实例。可以使用 sender 参数来访问和操作触发事件的控件,例如设置控件属
        性、调用控件的方法等。

        private void treeView1_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e)
        {
            TreeView treeView = (TreeView)sender;  // 将 sender 强转为 TreeView 类型
            // 在这里使用 treeView 控件来操作触发事件的控件
        }


        
        第二个参数e是一个类型为 TreeNodeMouseClickEventArgs 的参数,它表示与节点鼠
        标点击事件相关的详细信息。
        TreeNodeMouseClickEventArgs 类提供了多个属性,可以用来获取有关节点鼠标点击
        事件的各种信息。对应常用属性:
        
            Node:获取与鼠标点击事件相关的 TreeNode 实例,表示事件发生的节点。
            Button:获取点击鼠标按钮的枚举类型,表示触发事件的鼠标按钮。
            Clicks:获取鼠标的点击次数。
            X 和 Y:获取鼠标点击事件发生时的相对于节点控件的 X 和 Y 坐标位置。
        
        例如:

        private void treeView1_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e)
        {
            TreeNode clickedNode = e.Node;
            MouseButtons mouseButton = e.Button;
            int clickCount = e.Clicks;
            int xPosition = e.X;
            int yPosition = e.Y;

            // 在这里使用这些属性来操作和处理节点鼠标点击事件
        }


    
    
    4、编码方式
    
        上面有一个隐形的问题,在a处没有指明编码方式,因为不同的txt可能编码方式不同。
        
        若不知道一个文本文件的编码方式,能否检测其编码呢?
        可以有三种方式:
        
        (1)推断编码:
            可以使用 `StreamReader` 的 `CurrentEncoding` 属性来获取读取器当前使用的编
            码方式。当通过读取器读取文件时,可以检查该属性的值以获取推断的编码方式。
            然而,这种方法并不完全可靠,因为它基于一些启发式算法进行推断。
        
        StreamReader (System.IO.Stream stream, bool detectEncodingFromByteOrderMarks);
            detectEncodingFromByteOrderMarks参数通过查看流的前四个字节来检测编码。 
            若文件以适当的字节顺序标记开头,它会自动识别UTF-8、little-endian Unicode、
            big-endian Unicode、little-endian UTF-32 和 big-endian UTF-32 文本。

        StringBuilder sb = new StringBuilder();
        Encoding encoding = null;
        using (StreamReader sr = new StreamReader(file, true))
        {
            char[] buffer = new char[1024];
            int bytesRead = 0;

            do
            {
                bytesRead = sr.Read(buffer, 0, buffer.Length);
                if (encoding == null)
                {
                    encoding = sr.CurrentEncoding;
                }
                string s = new string(buffer, 0, bytesRead);
                sb.Append(s);
            } while (bytesRead > 0 && bytesRead >= 1024);

            MessageBox.Show(sb.ToString());
        }


        上面sr加了参数true就具有推断功能。第一次读前为null,读取时,StreamReader
        将推断出编码方式,并应用在后继的读取中。
        
        
        (2)穷举推算
            
            利用编码或解码出错时的回退处理机制来推测。

            Encoding.GetEncoding(encodingName, EncoderFallback.ExceptionFallback, 
                        DecoderFallback.ExceptionFallback) 


            这个编码指定encodingName编码时,无论是编码还是解码,都会抛出异常。
            如果不异常,多半是正确的编码。
            
            因此把所有已知的编码穷举列出,一个一个去试,没有异常的就是正确编码。

        private Encoding CodeMethod(string file)
        {
            string[] encodingNames = { "utf-8", "utf-16", "utf-32", "unicodeFFFE", "big-endian-utf-32" };

            // 尝试每种编码,检查第一个字符是否有效
            foreach (string encodingName in encodingNames)
            {
                Encoding encoding = Encoding.GetEncoding(encodingName, EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback);
                try
                {
                    byte[] buffer = new byte[1024];
                    int bytesRead = 0;
                    using (FileStream fs = new FileStream(file, FileMode.Open, FileAccess.Read))
                    {
                        bytesRead = fs.Read(buffer, 0, buffer.Length);
                    }
                    string text = encoding.GetString(buffer, 0, bytesRead);
                    return encoding;
                }
                catch (DecoderFallbackException)
                {
                    continue;
                }
            }

            return Encoding.Unicode;//应该是未知类型,暂时这样处理
        }


        上面encodingNames并没有穷举完,要穷举完可以这样:

        List<string> list = new List<string>();
        foreach (EncodingInfo encodingInfo in Encoding.GetEncodings())
        {
            list.Add(encodingInfo.Name);
        }
        string[] encodingNames = list.ToArray();


        
        
        (3)使用第三方库
            还有一些第三方库可以用于检测文本文件的编码。例如,`CharsetDetector` 
            是一个常用的库,它可以根据文本内容推断编码方式。您可以在 NuGet 包管
            理器中搜索并安装适用于您的应用程序的库。
            
        (4)使用外部工具
            除了使用代码,您还可以借助一些外部工具来检测文件的编码方式。例如,
            `file` 命令在 Linux 系统上可以用于检测文件的编码,`chardet` 是一个
            跨平台的命令行工具,可以用于检测文件的编码。
            
        总结:编码检测并不总是准确的,因为文件本身可能缺少明确的标识来指示其编码方
                式。就象通过人名来判断人的性别一样,不总是准确的。
                在某些情况下,最好的解决方案可能是人工检查文件并尝试使用不同的编码
                方式。
    
        回到3项中的资源管理器a处编码处,因此,根据得出的编码写出:

        Encoding encoding = CodeMethod(e.Node.Tag.ToString());
        textBox1.Text = File.ReadAllText(e.Node.Tag.ToString(),encoding);


        可以一定程度上正确地解码。
    


五、文件编码


    1、为什么会产生乱码?
    
        答:产生乱码的原因(只有文本文件才会乱码):文本文件存储时采用的编码,与读
            取时采用的编码不一致,就会造成乱码问题。
            
            解决:采用统一的编码就OK.
            
        什么是文本文件?
        答:文本文件是由纯文本字符组成的文件,它们包含了可被计算机读取和编辑的文本
        内容。文本文件通常以扩展名为 “.txt” 的形式保存,但并不局限于这个扩展名。
        
        Word文档(.docx、.doc等)不是纯文本文件,而是富文本文件。Word文档除了包含
        文本内容外,还可以包括格式、排版、图像、表格、样式等丰富的数据和标记。这
        些富文本文件需要特定的应用程序(如Microsoft Word)才能正确地打开、编辑和
        显示。
        
        文本文件是以简单的字符表示的,每个字符都有对应的数字编码。常见的文本文件
        编码方式包括ASCII、UTF-8、UTF-16等。每个字符被存储为相应的编码序列,使之
        能够被计算机读取和处理。
        
        
    2、什么是文本文件编码?
        
        答:文本文件有不同的存储方式,将字符串以什么样的形式保存为二进制,这个就
        是编码,如UTF-8、ASCII、Unicode等.
        
        如果出现乱码一般就是编码的问题,文本文件相关的函数一般都有一个Encoding类
        型的参数,取得编码的方式:
            Encoding.Default、Encoding.UTF8、Encoding.GetEncoding("GBK”)等.
        
        文件编码(码表)
            ASCII:英文码表,每个字符占1个字节(正数)。
            GB2312: 兼容ASCII,包含中文。每个英文占一个字节(正数),中文占两个字
                    节(负数)
            GBK:简体中文,兼容gb2312,包含更多汉字。英文占1个字节(正数),中文
                    两个(1个负数,1个可正可负)
            GB18030:对GB2312、GBK和Unicode的扩展,覆盖绝大部分中文字符,包括简
                    体字、繁体字、部分生僻字和各种中文标点符号。
                    它是双字节和多字节混合的编码方式。
            Big5: 繁体中文
            Unicode: 国际码表,中文英文都站2个学节
            UTF-8:国际码表,英文占1个字节,中文占3个字节
        
            ANSI:American National Standards Institute(美国国家标准协会)它定
                义了一系列的字符编码标准。常指代Windows操作系统的默认字符编码,
                即ANSI编码。
                
            ANSI编码最常见的是ANSI字符集,也叫作Windows-1252字符集,它是ASCII字符
        集的扩展,包括了一些特殊字符、货币符号、重音符号等。ANSI字符集只能表示少数
        语言的字符,对于其他非英语语言的字符,如中文、日文和韩文等,无法完全表示。
        
            在Windows上,当打开一个使用ANSI编码保存的文本文件时,系统会自动识别并
        选择合适的字符集来解码文件。如果文件中包含汉字,系统会使用GB2312字符集来
        解码,并将汉字正确地显示出来。
        
            注意,ANSI和GB2312都只是一种局限的编码方式,无法适用于全球范围内的所有
        字符。为了更好地表示和处理不同语言的文字,推荐使用Unicode编码,如UTF-8。
            ANSI编码不是一个统一的编码标准,它的具体实现在不同的国家/地区和操作系统
        中可能会有所不同。
        
        
        Unicode编码有几种不同的实现方式,包括以下几种常见的编码方案:
            UTF-8(8-bit Unicode Transformation Format):UTF-8是一种可变长度的编
                    码方式,用1至4个字节来表示字符。对于ASCII字符,使用1个字节表
                    示,而对于其他非ASCII字符,根据需要使用2至4个字节。UTF-8兼容
                    ASCII编码。
            UTF-16(16-bit Unicode Transformation Format):UTF-16使用定长的16位
                    (2个字节)来表示字符。对于基本多文种平面(BMP)中的字符,使
                    用2个字节表示,而对于其他辅助平面中的字符,则使用4个字节表示。
            UTF-32(32-bit Unicode Transformation Format):UTF-32使用32位(4个字
                    节)来表示每个字符。UTF-32固定使用4个字节来表示所有字符,不论
                    它们属于哪个平面。
        除了上述几种常见的编码方案,还有一些其他的Unicode编码实现方式,比如UTF-7
        (7-bit Unicode Transformation Format)和UTF-EBCDIC(EBCDIC是IBM的一种字
        符编码方案)等,但它们较少被使用。
        
        注意,这些编码方案都是Unicode编码的不同实现方式,它们的目标都是为了能够
        表示全球范围内的字符和符号,但采用不同的编码方式和字节序。UTF-8是目前最
        常用的编码方式,因为它在广泛应用的同时,还具有较小的存储空间和网络传输
        开销。
        
        
    3、Encoding类
        
        Encoding类是C#中用于处理字符编码和转换的类。它位于System.Text命名空间中,是
        一组静态方法和属性的集合,用于在不同的字符编码之间进行转换、编码和解码操作。
        
        1)常用成员和功能:
        
        (1)Encoding.GetEncoding()
            通过指定编码名称或编码标识符来获取对应的Encoding对象。
            例如,可以使用以下方式获取UTF-8编码对象:
            Encoding utf8 = Encoding.GetEncoding("UTF-8");
        
        (2)Encoding.Default
            表示系统默认字符编码的Encoding对象。在Windows中,默认编码一般为ANSI编
            码(如Windows-1252),但在不同的操作系统和环境中可能会有所不同。可以
            使用Encoding.Default来获取默认编码对象。
            
        (3)GetBytes和GetString
            用于在字节数组和字符串之间进行编码和解码操作。
            GetBytes方法将字符串转换为字节数组,可指定目标编码;
            GetString方法将字节数组转换为字符串,同样可指定源编码和目标编码。
            

        string text = "Hello, world!";
        byte[] utf8Bytes = Encoding.UTF8.GetBytes(text); // 字符串转换为UTF-8编码的字节数组
        string decodedText = Encoding.UTF8.GetString(utf8Bytes); // UTF-8编码的字节数组转换为字符串


        
        Encoding.GetEncoding方法还提供了一些常见的字符编码的预定义常量,
        如UTF8、ASCII、Unicode等。
        

        Encoding utf8 = Encoding.UTF8; // UTF-8编码对象
        Encoding ascii = Encoding.ASCII; // ASCII编码对象
        Encoding unicode = Encoding.Unicode; // Unicode编码对象


        
        2)一般可以在Encoding指定编码,可以智能提示找出。但有些无法找出,需要用“名
            字”指定,比如GB2313

        Encoding encoding=Encoding.GetEncodings("GB2312");
        
            Encoding.GetEncodings(),则是所有编码。
        EncodingInfo[] infos=Encoding.GetEncodings();
        
        下面把所有的编码写到一个文本文件中:
        private static void Main(string[] args)
        {
            EncodingInfo[] infos = Encoding.GetEncodings();
            foreach (EncodingInfo info in infos)
            {
                File.AppendAllText(@"E:\1.txt", string.Format($"{info.CodePage},{info.DisplayName},{info.Name}\r\n"));
            }
            Console.ReadKey();
        }


        上面的例子引发下面的“血案”:
        (1)@与$
            $插值字符串
                允许您在{}中直接嵌入变量,并且会在运行时自动进行变量的求值和替换。

            string name = "Alice";
            int age = 30;
            
            // 使用插值字符串将变量{name}和{age}嵌入到字符串中
            string message = $"My name is {name} and I am {age} years old.";
            Console.WriteLine(message);
            // 输出:My name is Alice and I am 30 years old.   

     
        
            @原始字符串
                允许在字符串中保留转义字符而不进行转义。

            string path1 = "C:\\Windows\\System32\\";
            string path2 = @"C:\Windows\System32\";

            Console.WriteLine(path1);
            Console.WriteLine(path2);


            
            注意:原始字符串中的双引号仍然需要进行转义,即使用两个双引号 "" 来表示
                一个双引号。

            string message = @"She said, ""Hello world!""";
            Console.WriteLine(message);//She said, "Hello world!"
            
            或者:message = "She said, \"Hello world!\"";


            
        (2)EncodingInfo信息
            CodePage:字符编码的标识号,用于指代不同的字符编码方案.
            DisplayName:字符编码的友好显示名称,通常用于向用户展示或描述该编码。
            Name:用于获取字符编码的名称.通常使用小写字母表示,如utf-8.
            
        (3)换行回车\r\n
            换行符表示方式\r\n。\r表示回车(Carriage Return),\n表示换行(Line Feed)
            
            \n\r 和 \r\n 在大多数情况下是等效的.为了确保跨平台的兼容性,推荐仍然
            使用 \r\n,它是标准的 Windows 平台上的换行符表示方式。
        
        (4)File.AppendAllText与FileAppendText的区别
            File.AppendAllText和File.AppendText方法是C#中用于将文本内容附加到指定文
            件的方法。
            
            AppendAllText方法属于System.IO命名空间。该方法接受一个文件路径和要附加
            的文本内容作为参数,将文本内容追加到指定文件的末尾。如果文件不存在,该
            方法会创建一个新的文件,并将文本内容写入文件。
            

            // 将文本内容追加到指定文件的末尾
            string filePath = "path/to/file.txt";
            string content = "This is the appended content.";
            File.AppendAllText(filePath, content);


            
            AppendText方法也属于System.IO命名空间。该方法接受一个文件路径作为参数,
            返回一个StreamWriter对象,您可以使用该对象向文件中写入文本内容。
            
            与File.AppendAllText不同,File.AppendText方法会在指定文件的末尾打开一
            个文本写入器(即StreamWriter),并返回该写入器对象。您可以使用该对象进
            行连续的写入操作,而不需要每次都重新打开和关闭文件。
            

            // 打开一个文本写入器,并将文本内容追加到指定文件的末尾
            string filePath = "path/to/file.txt";
            string content = "This is the appended content.";
            using (StreamWriter writer = File.AppendText(filePath))
            {
                writer.WriteLine(content);
            }


            注意:使用`File.AppendText`打开的文件写入器是在using语句块中使用的,
                    所以会在结束块时自动关闭文件。
            
            简单说区别:
                两者都是在未尾追加文本,若文件不存在均自动创建一个。
                
                区别:AppendAllText方法没有返回值,且自动关闭追加的文件。
                            接收两个参数filepath与content
                      AppendText方法返回一个StreamWrite,且需要干预进行关闭文件。
                            接收一个参数filepath。文本参数在返回值中操作。

        private static void Main(string[] args)
        {
            Stopwatch sw = new Stopwatch();
            sw.Start();
            for (int i = 0; i < 1000; i++)
            {
                File.AppendAllText(@"E:\1.txt", i.ToString("000") + "\r\n");
            }
            sw.Stop();
            double d1 = sw.ElapsedMilliseconds;

            sw.Restart();
            using (StreamWriter swr = File.AppendText(@"E:\2.txt"))//using自动关闭swr
            {
                for (int i = 0; i < 1000; i++)
                {
                    swr.WriteLine(i.ToString("000"));//文本在返回值中追加
                }
            }
            sw.Stop();
            double d2 = sw.ElapsedMilliseconds;
            Console.WriteLine($"{d1},{d2}");//330,2

            Console.ReadKey();
        }


            可以看到两者的效率相差160几倍,就是因为AppendAllText反复关闭文件。
            而AppendText并没有关闭(因为它还需返回值来追加文本).
            


六、File类


    1、File类的常用静态方法: (Filelnfo*)
    
        void AppendAllText(string path, string contents)
                                将文本contents附加到文件path中
        bool Exists(string path)判断文件path是否存在
        
        string[] ReadAllLines(string path) 读取文本文件到字符串数组中
        string ReadAllText(string path) 读取文本文件到字符串中
        
        void WriteAlIText(string path, string contents)
                                将文本contents保存到文件path中,会覆盖旧内容。
        WriteAllLines(string path.strinall contents)
                                将字符串数组逐行保存到文件
    
    2、File类的方法1
        
        File.Copy(source,targetFileName,true)
                文件拷贝,true表示当文件存在时"覆盖",如果不加true,则文件存在报异常
        File.Exists(path);  //判断文件是否存在.返回bool
        File.Move(source,target) ;//移动(剪切),思考如何为文件重命名?
                                        文件的剪切是可以跨磁盘的。
        File.Delete(path) ;  //删除。如果文件不存在? 不存在,不报错
                            注意:Directory.Delete删除不存在的目录,将引发异常。
        File.Create (path) ; //创建文件
    
    
    3、File类的方法2:操作文本文件
    
        File.ReadAllLines (path,Encoding.Default) ;//读取所有行,返回string[]
        File.ReadAllText(path,Encoding.Default);//读取所有文本返回string
        File.ReadA11Bytes (path) ;//读取文件,返回byte[]把文件作为二进制来处理。
        
        File.WriteAllLines(path,new string[4],Encoding.Default);//将string数据按行写入文件
        File.WriteAllText (path,string);//将字符串全部写入文件
        File.WriteAl1Bytes(path,new byte[5]) ;//将byte[]全部写入到文件
        
        File.AppendAllText(path,string) //将string追加到文件(无返回值)且关闭文件
        File.AppendText(path);//打开文件。返回StreamWriter,进行追加文件,
                                需手工关闭文件
    
    
    4、File类的方法3: 快速得到文件流
    
        Filestream fs=File.Open(); //返Filestream
        Filestream fs-File.OpenRead() ;//返回只读的Filestream
        Filestream fs=File.OpenWrite() ;//返回只写的Filestream
        Filestream fs=new Filestream(参);
        
        Stream(所有流的父类,是一个抽象类。)
        文件操作的类都在system.Io.*;
    
    
    5、问:什么叫流(Stream)?是怎样产生这个概念的?
    
        答:在C#中,"流"(Stream)这个词用来表示一种连续的数据传输方式。可以将其想
        象成一条河流,数据像水一样从一个地方流向另一个地方。这个概念之所以被称为
        "流",是因为它可以让我们在处理数据时,不需要一次性处理整个数据集。
        
        处理数据的方式就像是从一端传入数据,然后在另一端逐渐接收和处理。这样我们就
        可以逐步处理大量的数据,而不会因为数据量过大而导致内存不足或性能下降。
        
        流的概念在计算机科学中具有悠久的历史,最早可以追溯到20世纪60年代的Unix操作
        系统。最初的设计是为了简化文件和设备之间的数据传输,后来发展成为处理各种数
        据源(如文件、网络和内存)的通用概念。
        
        流的好处包括:
        (1)节省内存:
            流式处理不需要将整个数据集加载到内存中,因此可以处理大量数据,而不会耗
            尽内存资源。
        (2)提高性能:
            读取和处理数据的速度可以根据实际需求进行调节,避免了不必要的等待。
        (3)灵活性:
            流提供了一个通用的接口,可以用于处理各种类型的数据源,如文件、网络数据
            或内存缓冲区。
        (4)易于组合:
            多个流可以组合在一起,以便在处理数据时执行多个操作,例如加密、压缩或编码。
        
        通俗说:每家每户要用水(数据)可以直接拉一车来用,但占用车和人力,如果安装
                成自来水管,这边输入,用户输出,小量多次,将水流(数据)传到用户。
                
    
    6、FileInfo类
        
        提供了一系列方法和属性,用于获取和操作文件的信息。常用方法和属性:

        1)常用方法:
            
            Create():创建一个新文件。
            Delete():删除文件。
            RenameTo(string destFileName):重命名文件。
                警告vs2022中已经没有此方法,请转用MoveTo()代替
            
            CopyTo(string destFileName):将文件复制到指定的目标位置。
            OpenRead():以只读方式打开文件流。
            OpenWrite():以写入方式打开文件流。
            
            GetAccessControl():获取文件的访问控制列表。
            MoveTo(string destFileName):将文件移动到指定的目标位置。已存在则异常。
                File.Move(src,dec)也同样是移动,但是覆盖
                上面两者Move都可跨磁盘,但Directory.Move不能跨磁盘。
            
            问:FileInfo.MoveTo()与File.Move()的区别是什么?
            答:两者都是移动,都可跨磁盘,都返回void。但:
            File中是静态方法,FileInfo是实例方法。前者两个参数,后者一个参数。
            最重要的是FileInfo移动后,会指向新的对象:

            FileInfo fi = new FileInfo(@"E:\1\1.txt");
            fi.MoveTo(@"E:\1\2.txt");
            Console.WriteLine(fi.Name);//指向新的2.txt


                
                
            问:file类与fileinfo类的区别
            答:两者均处理文件,均属system.IO命名空间中。
             (1)静态方法与实例方法:
                File类主要提供静态方法,用于直接操作文件。例如,读取文件内容、创建
            文件、删除文件、移动文件等。这些操作都可以直接在File类上进行,不需要创
            建对象实例。

            string content= File.ReadAllText("filePath");
            File.Delete("filePath");


                FileInfo类则是一个具体的对象,表示一个文件。要使用FileInfo类的方法,
            首先需要创建一个FileInfo对象,然后在该对象上调用相应的实例方法。

            FileInfo fileInfo=newFileInfo("filePath");
            string content= fileInfo.OpenText().ReadToEnd();fileInfo.Delete();


            
            (2)性能:
                File类的静态方法在每次调用时都会访问磁盘,可能会导致性能下降。特别
            是当需要对同一文件执行多个操作时,使用File类可能会导致不必要的性能损失。
                FileInfo类将文件信息缓存在内存中,因此在需要多次操作同一文件时,使
            用FileInfo类可能会更高效。例如,获取文件属性、读取文件内容、修改文件属
            性等操作。
            
            因此,例如file.move与fileinfo.move,后者是实例方法。

            File.Move("sourceFilePath", "destinationFilePath");
            
            FileInfo fileInfo = new FileInfo("sourceFilePath");//需要实例化
            fileInfo.MoveTo("destinationFilePath");
            
            又如file.delete与fileInfo.delete,后面是实例方法。
            
            FileInfo fileInfo = new FileInfo("example.txt");//实例化
            fileInfo.Delete();
            
            File.Delete("example.txt");//静态方法


            
            同样File.Create与FileInfo.Create,后者也为实例方法。两者都返回FileStream。
            唯一细微的差别就是,若单独创建,用静态简洁些。若已经有实例化对象,用
            FileInfo更顺手一些。
            
            麻烦的是,两者都要手动用using或close()进行关闭这个流。
            
            问:一个打开的流(如FileSteam),不进行手动关闭它,会发生什么情况?
            答:有下面不好情况发生:
            (1)文件锁定:如果FileStream流没有被正确关闭,该文件可能仍然被进程持有,
                而其他进程或程序可能无法对该文件进行读取、写入或删除等操作,直到当
                前进程关闭。
                
            (2)资源泄漏:未关闭的FileStream流可能导致资源泄漏。FileStream是一个占
                用系统资源的对象,如果没有正确释放,可能会导致内存泄漏或其他资源相
                关问题。
            
            (3)数据丢失:若在FileStream流关闭之前,对文件进行的写入操作可能无法完
                全刷新到磁盘上,从而导致数据丢失。
                
            
            
        2)常用属性:
        
            Name:获取文件的名称(不含路径)。
            FullName:获取文件的完整路径(含文件名)。
            
            DirectoryName:获取文件所在的目录名称。返回string
                    Directory:同上。但返回的是DirectoryInfo实例
            Length:获取文件的大小。返回long.
            CreationTime:获取文件的创建时间。
            
            LastWriteTime:获取文件的最后一次写入时间。
            LastAccessTime:设置或获取最后一次访问时间。返回DateTime
        

        FileInfo fileInfo = new FileInfo("example.txt");// 创建一个FileInfo对象
        if (fileInfo.Exists)// 检查文件是否存在
        {
            long fileSize = fileInfo.Length;// 获取文件的大小
            DateTime creationTime = fileInfo.CreationTime;// 获取文件的创建时间
            fileInfo.MoveTo("newfile.txt");// 重命名文件
            fileInfo.CopyTo("copy.txt");// 复制文件

            fileinfo.ReName()
            fileInfo.Delete();// 删除文件
        }


        
        技巧:
        a. 在操作文件之前,建议使用Exists检查文件是否存在,以避免潜在的错误。
        b. 在处理文件路径时,使用Path类的方法来拼接、合并和解析路径。
        c. 文件操作可能涉及到权限和访问控制的问题,确保程序在进行文件操作时具备足
            够的权限,避免出现访问被拒绝的错误。
        d. 在多线程环境下操作文件时,可以使用lock语句来确保线程安全性,避免多个线
            程同时操作同一个文件导致的冲突。
        e. 在处理大文件时,考虑使用流式操作,以避免一次性加载整个文件到内存中。例
            如,使用OpenRead()方法获取文件的流,进行逐行或逐块地处理数据。
    
    
    7、DirectoryInfo类(System.IO)
    
        提供了一种方便的方法来操作目录和子目录,如创建、移动、删除目录,以及获取目
        录的属性和子目录等。
        
        (1)创建一个`DirectoryInfo`对象

        DirectoryInfo dirInfo=new DirectoryInfo(@"C:\ExampleDirectory");


        
        (2)创建目录

        if(!dirInfo.Exists)
        {dirInfo.Create();}


        
        (3)获取目录属性

        DateTime creationTime=dirInfo.CreationTime;
        DateTime lastAccessTime=dirInfo.LastAccessTime;
        DateTime lastWriteTime=dirInfo.LastWriteTime;


        
        (4)获取子目录

        DirectoryInfo[] subDirectories=dirInfo.GetDirectories();


        
        (5)获取目录中的文件
        

FileInfo[] files =dirInfo.GetFiles();


        
        (6)移动目录
      

  dirInfo.MoveTo(@"C:\NewDirectory");


        
        (7)删除目录
        

dirInfo.Delete(true);//参数为true表示递归删除子目录和文件


        
        技巧:
        
        (1)使用DirectoryInfo而不是Directory类操作目录时,可以避免不必要的安全检查。
            DirectoryInfo对象在创建时执行一次安全检查,而Directory类的静态方法每次
            调用时都会执行安全检查。
        (2)若要在同一目录下执行多个操作,使用DirectoryInfo类可以提高性能,因为它会
            缓存有关目录的信息。
        (3)在遍历目录和文件时,可以使用EnumerateDirectories和EnumerateFiles方法替代
            GetDirectories和GetFiles方法。这样可以逐个返回目录或文件,而不是一次性
            返回所有结果,从而提高性能。
        (4)如果需要对文件和目录进行筛选,可以在GetDirectories、GetFiles、EnumerateD
            irectories和EnumerateFiles方法中使用搜索模式参数。例如:

            //获取所有.txt文件
            FileInfo[] txtFiles=dirInfo.GetFiles("*.txt");


        (5)在操作文件系统时,请注意处理可能出现的异常,例如IOException、Unauthoriz
            edAccessException等。这有助于提高代码的稳定性和健壮性。
    
        问:Directory类与DirectoryInfo类的区别是什么?
        答:类似前面的File与FileInfo一样。前面使用静态方法,后面使用实例方法。

            DirectoryInfo di = new DirectoryInfo(@"E:\1");
            di.MoveTo(@"E:\2");
            Console.WriteLine(di.Name);//已经指向目录E:\2

            Directory.Move(@"E:\2", @"E:\1");
            Console.WriteLine(di.FullName);//仍为E:\2不报错


        
        举例:

        string[] d = Directory.GetLogicalDrives();
        foreach (var item in d)
        {
            DriveInfo di = new DriveInfo(item);//C,C:,C:\  都正确
            Console.WriteLine(di.DriveType);//判断硬盘,光盘,移动盘,网络盘
        }
        Console.WriteLine((new DriveInfo("E:")).DriveType);


    
    
    8、DriveInfo类
        
        注意:没有Drive类,只有DriveInfo类.
        
        DriveInfo类是用于获取和操作磁盘驱动器信息的类。可以获取磁盘驱动器的容量,
        可用空间,卷标和驱动器类型等信息。
        
        常用属性和方法:
            (1)Name属性:获取驱动器的名称,如"C:\"。
            (2)DriveType属性:获取驱动器的类型,如Fixed、CDRom等。
            (3)AvailableFreeSpace属性:获取驱动器的可用空间,以字节为单位。
            (4)TotalFreeSpace属性:获取驱动器的总可用空间,以字节为单位。
            (5)TotalSize属性:获取驱动器的总大小,以字节为单位。
            (6)VolumeLabel属性:获取或设置驱动器的卷标。

        private static void Main(string[] args)
        {
            DriveInfo[] dis = DriveInfo.GetDrives();
            foreach (DriveInfo di in dis)
            {
                Console.WriteLine($"{di.Name},{di.DriveType}");
                if (di.IsReady)//是否准备好,例如,光盘驱动中已有光盘,移动驱动中已有U盘
                {
                    Console.WriteLine($"\t{di.TotalSize},{di.AvailableFreeSpace},{di.VolumeLabel}");
                }
            }
            Console.ReadKey();
        }


    
        技巧:
        
        (1)异常处理:使用DriveInfo类时,可能会遇到未准备好的驱动器或无效路径的情况。
            因此在访问驱动器属性时,最好通过异常处理来处理可能的异常,避免程序崩溃。
            
        (2)判断驱动器是否就绪:在访问驱动器的属性之前,最好先判断驱动器是否就绪
            (IsReady属性)。如果驱动器未就绪,则可能无法读取有效的驱动器属性。
            
        (3)提升性能:如果在一个循环中多次访问相同的驱动器属性,可以将DriveInfo实例
            保存在一个变量中,并在需要时直接使用该变量,这样可以提高性能,避免多次
            访问驱动器属性。

            DriveInfo drive = new DriveInfo("C:");
            for (int i = 0; i < 10; i++)
            {
               Console.WriteLine("总大小: {0} 字节", drive.TotalSize);
               Console.WriteLine("可用空间: {0} 字节", drive.AvailableFreeSpace);
            }


           
        (4)路径格式:在创建DriveInfo实例时,可以使用驱动器的根目录的路径,例
            如"C:\"、"D:\"等。注意,路径应使用双反斜杠或单斜杠进行转义。

           DriveInfo drive = new DriveInfo("C:\\");


        
        (5)磁盘卷标:使用VolumeLabel属性可以获取或设置驱动器的卷标。对于某些特定
            的驱动器类型,可能无法设置卷标。

            DriveInfo drive = new DriveInfo("C:");
            if (drive.IsReady)
            {
               Console.WriteLine("当前卷标: {0}", drive.VolumeLabel);
               // 设置卷标
               drive.VolumeLabel = "MyDrive";
            }


         
        (6)权限问题:在某些情况下,可能需要以管理员权限运行程序才能访问某些驱动器
            属性,如系统盘。在这种情况下,可以以管理员身份运行程序或者使用相关权限
            进行授权。
    


七、文件流


    1、拷贝文件的两种方式:
            将源文件内容全部读到内存中,再写到目标文件中;读取源文件的1KB内存,写到
            目标文件中,再读取源文件的1KB内存,再写到自标文件中...如此循环直到结束.
            
            第二种方式就是一种流的操作。
            
            两个大水缸,把一个缸中的水倒入另一个水缸中。有两种方式:
            (1)直接把一个缸中的水倒入另一个缸中;
            (2)用一个瓢来把一个缸中的水分多次舀到另一个缸中。
            
        用File.ReadAllText、File.WriteAllText进行文件读写是一次性读、写。如果文件
            非常大,会占内存且速度慢。需要读一行处理一行的机制,这就是流(Stream)。
            Stream会只读取要求的位置、长度的内容。
            
        Stream不会将所有内容一次性读取到内存中,它有一个指针,指针指到哪里才能读、
            写到哪里。
            
        流有很多种类,文件流是其中一种。FileStream类:
            new FileStream(“c:/a.txt”filemode, fleaccess)后两个参数可选值及含义自
            己看。FileStream可读可写。可以使用File.OpenRead、File.OpenWrite这两个
            简化调用方法。
            
        byte[]是任何数据的最根本表示形式,任何数据最终都是二进制。
        
            问:流Stream可以看作字节流吗?
            
            答:是的,流(Stream)可以被看作是字节的序列。流提供了对数据的读取和写
            入操作,可以从一个地方读取数据并将其传输到另一个地方。流的操作可以对
            字节流、字符流或自定义的流进行处理,但最基本的流是字节流。
            
            字节流(Stream)是指从数据源读取和写入字节的流。它可以用于处理二进制数
            据,如图像、视频、音频或任何其他形式的文件。字节流提供了一种读取和写
            入原始字节的方法,可以精确地操作二进制数据。在C#中,可以使用字节流类
            (如FileStream)来处理字节流。
            

            string s = "功行如激流,心念常清修。";
            byte[] bs = Encoding.UTF8.GetBytes(s);
            string newS = Encoding.UTF8.GetString(bs);


            
        FileStream的Position属性为当前文件指针位置,每写一次就要移动一下Position,
            以备下次写到后面的位置。Write用于向当前位置写入若干字,Read用于读取若
            干字节。(*)
    
    
    
    2、FileStream读写文件
        
        它按字节顺序从文件读取数据或向文件写入数据的方式。
        
        方法:(1)建立读或写的文件流;(2)使用读或写的文件流;(3)关闭流释放资源。
        
        例1:写入文件

        FileStream fs = new FileStream(@"E:\1.txt", FileMode.Create, FileAccess.Write);
        string s = "众生本来具足佛性,只是须发光明不显耳";

        byte[] byts = Encoding.UTF8.GetBytes(s);
        Console.WriteLine(s.Length);//18
        Console.WriteLine(byts.Length);//54

        fs.Write(byts, 0, byts.Length);//a

        fs.Flush();//b
        fs.Close();//c
        fs.Dispose();//d


        上面s长度18经UTF8编码后成54,因为UTF会将1个汉字转为2-4个字节。所以在a处转换时
        一般使用缓冲(byts)的最大长度,当然如果也可取中间部分。比如,缓存为1000,但实际
        字节只有200,后面800就是空的,那么这里就不能是缓冲的长度,只能是200.
        
        上例byts.Length试用36,则输出部分汉字。但若35但全部乱码,因为这时进而字节不再是
        有规则,UTF8解码时主乱了。
        
        b处是清空缓冲。
        在写入大量数据到文件中时,数据往往会首先存储在内存中的缓冲区中,而不是立即写入
        磁盘。这样做的目的是优化性能,减少频繁的磁盘I/O操作。但是,对于某些特定的场景
        和需求,你可能希望立即将数据写入磁盘并且确保数据已经完全写入,这时就需要显式
        调用Flush()方法。例如,在写入文件后需要确保其他进程或系统能够立即访问到最新的
        数据。
        
        注意:调用Flush()方法会强行立即将缓冲写到文件中,会导致额外的磁盘I/O操作,可能
            会影响性能。因此,在一般的情况下,不需要显式调用Flush()方法,在关闭
            FileStream的时候会自动刷新数据。
            
        其实,在filesteam.Close()时会自动调用Flush()方法,所以a处是不必要的代码。
        
        另外close与dispose关闭方法类似,用了close可以就必dispose。
        但一般都将filesteam用在using语句,它将自动隐式调用dispose来关闭释放对象。
        
        close与dispose的细微差异在于:
        Close()方法关闭文件句柄,释放与文件相关的资源,但对象本身仍然存在于内存中。
        Dispose()方法不仅关闭文件句柄,还释放了对象本身占据的内存空间,包括底层资源
                和缓冲区等。
        
        因此最后三句,实际上只要dispose就可以了。而这又常被用using直接代替。
        
        例2:读取文件

        using (FileStream fs = new FileStream(@"E:\1.txt", FileMode.Open, FileAccess.Read))
        {
            byte[] buffer = new byte[fs.Length];//a:new byte[fs.Length];
            int intRead = fs.Read(buffer, 0, buffer.Length);//b
            string s = Encoding.UTF8.GetString(buffer);//c
            Console.WriteLine(s);
        }


        上面a处若用fs.Length,最后输出字符串前面可能会有?号,
        
        为什么可能有?号呢?
        
        请参看后面的“BOM”介绍。
        
    
    3、    使用FileStream进行大文件拷贝
    

        string src = @"E:\八段锦.mp4";
        string des = @"E:\1.mp4";
        using (FileStream fread = new FileStream(src, FileMode.Open, FileAccess.Read))
        {
            using (FileStream fwrite = new FileStream(des, FileMode.Create, FileAccess.Write))
            {
                byte[] buffer = new byte[1024 * 1024 * 10];
                int bytesRead;
                double count = 0;

                while ((bytesRead = fread.Read(buffer, 0, buffer.Length)) > 0)
                {
                    fwrite.Write(buffer, 0, bytesRead);
                    //下面显示进度
                    count = count + bytesRead;
                    Console.Clear();
                    Console.WriteLine($"{(count / fread.Length):P0}");
                }
            }
        }
        Console.WriteLine("OK");


        缓冲buffer的大小根据拷贝文件的大小灵活掌握,上面控制成10M。
        
        
        问:对于缓冲buffer数组一般设置多大?
        答:对于大文件读写:通常较大的缓冲区能够提高读取或写入速度。根据测试和经
        验,选择一个通常在 8KB 到 128KB 之间的缓冲区大小。
        
        尽量避免过小的缓冲区:过小的缓冲区大小可能导致频繁的磁盘I/O操作,降低性能。
        
        
        问:有些读取或写入并没有设置缓冲buffer,这又是怎么回事?
        答:如果没有写明缓冲区,尽管不同的系统和环境,隐式的缓冲区不同,但一般情况
        下,默认StreamReader默认缓冲区8K,FileStream默认缓冲为4K。官方文档并没明确
        说明,默认缓冲区大小是根据运行时环境和底层数据流的类型自动设置的。

        using (StreamWriter sw = new StreamWriter(@"E:\1.txt", true))
        {
            for (int i = 0; i < 1000; i++)
            {
                sw.WriteLine(i.ToString("000"));
            }
        }

        using (StreamReader sr = new StreamReader(@"E:\1.txt", Encoding.Default))
        {
            string line;
            while ((line = sr.ReadLine()) != null)
            {
                Console.WriteLine(line);
            }
        }


        上面写入与读取分别使用的是隐式的缓冲。
        
        总结:StreamReader 和 StreamWriter 是高级别的文本读写操作工具,内置了隐式
            缓冲区处理机制。而 FileStream 则提供了对底层文件流的直接控制,需要显式
            设置缓冲区大小以满足性能要求。
            
        简介一下using用法:
            (1)using 语句用于管理实现了 IDisposable 接口的对象。
            (2)using 语句块中创建的对象只在该代码块作用域内有效。
            (3)无论是正常结束还是发生异常,using语句块结束时会自动调用对象的Dispose
                    方法,释放相关资源。
            (4)using 语句还可以同时管理多个需要释放资源的对象。
            
        注意:
            (1)需要释放的对象应放在括号 () 内,多个对象用逗号隔开。
            (2)花括号{}不能为空,即使无代码也需要一个占位符(例如一个空的注释)。
            
            
    4、练习:文件加密
        文件流操作的是字节数组,现在对其加密。也就是文件加密,每一个字节,就是数组
        中的第一个元素(每一位用255-r) 。解密的时候,就再次用255-r得到原来的字节。
        由于两者的运算是一样的,所以,加密就是解密,解密就是加密。例如,文件流中有字
        节数组,其中第一个元素字节是250,加密时:255-250=5,存储加密时用5;当解密时,
        再用255-5=250,即得到原来的字节元素,把正常的字节数组再显示出来,就是解密。

        private static void Main(string[] args)
        {
            JiaMI(@"E:\1.txt", @"E:\.txt");
            string s = File.ReadAllText(@"E:\2.txt", Encoding.Default);//乱码
            Console.WriteLine(s);

            JiaMI(@"E:\2.txt", @"E:\3.txt");
            s = File.ReadAllText(@"E:\3.txt", Encoding.Default);//正常
            Console.WriteLine(s);

            Console.ReadKey();
        }

        private static void JiaMI(string scr, string des)
        {
            using (FileStream fsr = new FileStream(scr, FileMode.Open, FileAccess.Read))
            {
                using (FileStream fsw = new FileStream(des, FileMode.Create, FileAccess.Write))
                {
                    byte[] buffer = new byte[1024 * 8];//8K
                    int bytesRead;
                    while ((bytesRead = fsr.Read(buffer, 0, buffer.Length)) > 0)
                    {
                        for (int i = 0; i < bytesRead; i++)
                        {
                            buffer[i] = (byte)(255 - buffer[i]);//a
                        }
                        fsw.Write(buffer, 0, bytesRead);
                    }
                }
            }
        }


        注意:上面a需要强行转换。
    
        在C#中,四则运算(加法、减法、乘法和除法)的操作数默认都是int类型。这意味
        着,如果参与运算的操作数是其他整数类型(如byte、short或long),它们在进行
        运算之前都会被隐式地转换为int类型。
        
        因此上面的减法结果是int类型,需要强行转换为(byte)。
    
    
    5、Filestream的参数介绍。
        
        (1)参数
        FileStream(string path, FileMode mode, FileAccess access, FileShare share)
        path:表示要操作的文件的路径,可以是绝对路径或相对路径。
        
        mode:指定文件的打开模式,可以是以下值之一:
            FileMode.CreateNew:创建一个新的文件,如果文件已存在则抛出异常。
            FileMode.Create:创建一个新的文件,如果文件已存在则覆盖。
            FileMode.Open:打开一个文件,如果文件不存在则抛出异常。
            FileMode.OpenOrCreate:打开一个文件,如果文件不存在则创建一个新的文件。
            FileMode.Append:打开一个文件用于追加内容,如果文件不存在则创建一个新的文件。
            
        access:指定对文件的访问权限,可以是以下值之一:
            FileAccess.Read:允许读取文件。
            FileAccess.Write:允许写入文件。
            FileAccess.ReadWrite:既可以读取也可以写入文件。
            
        share:指定与其他程序共享文件的方式,可以是以下值之一:
            FileShare.Read:允许其他程序打开并读取文件。
            FileShare.Write:允许其他程序打开并写入文件。
            FileShare.ReadWrite:允许其他程序打开并读写文件。
            FileShare.None:不允许其他程序打开文件。
        
        (2)快速创建文件流。

        FileStream fsr = File.OpenRead(filepath);//相当于: 
                FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
        
        FileStream fsw = File.OpenWrite(filepath);//相当于:
            FileStream(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None);


        
    6、BOM(Byte Order Mark)字节顺序标记
        
        BOM是一个特殊的字节序列,通常用于标识Unicode文本文件的编码方式和字节顺序。
        
        BOM通常在保存Unicode文本文件时作为文件的开头几个字节存在。它可以用来指示文件的
        编码格式,例如UTF-8、UTF-16等,并标识字节的顺序,例如大端序(Big Endian)或小
        端序(Little Endian)。
        
        BOM在C#中经常用于读取和写入Unicode文本文件时,以确保正确的编码和字节顺序。在
        读取文本文件时,可以使用.NET中的编码类(例如UTF8Encoding、UnicodeEncoding等)
        来处理BOM并正确解码文件。而在写入文本文件时,可以使用这些编码类的相应方法来添
        加BOM并以正确的编码方式保存文件。
        
        注意:并非所有的Unicode文本文件都包含BOM。有些文件可能不包含BOM,而是仅仅依赖
            于文件的格式或者协议来指定编码和字节顺序。因此,在处理Unicode文本文件时,
            建议根据实际情况来选择是否使用BOM。
        
        
        因此,有BOM的文件前面几个字节是BOM,后面才是真实的内容。
        
        根据不同的BOM字节序标记,可以判断以下常见的Unicode编码文件:
        UTF-8 编码文件:UTF-8 BOM 的字节序列为 0xEF, 0xBB, 0xBF。
        UTF-16 Big Endian 编码文件:UTF-16 Big Endian BOM 的字节序列为 0xFE, 0xFF。
        UTF-16 Little Endian 编码文件:UTF-16 Little Endian BOM 的字节序列为 0xFF, 0xFE。
        UTF-32 Big Endian 编码文件:UTF-32 Big Endian BOM 的字节序列为 0x00, 0x00, 0xFE, 0xFF。
        UTF-32 Little Endian 编码文件:UTF-32 Little Endian BOM 的字节序列为 0xFF, 0xFE, 0x00, 0x00。
        
        注意:并非所有的Unicode编码文件都使用BOM作为标志,而且有些还会是自定义的BOM。
        
        因此根据上面的判断标准:

        string filePath = @"E:\1.txt";
        byte[] bytes = File.ReadAllBytes(filePath);

        //根据BOM判断
        if (bytes.Length >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF)
        {//UTF-8 BOM
            Console.WriteLine("文件编码为 UTF-8");
        }
        else if (bytes.Length >= 2 && bytes[0] == 0xFE && bytes[1] == 0xFF)
        {//UTF-16 Big Endian BOM
            Console.WriteLine("文件编码为 UTF-16 Big Endian");
        }
        else if (bytes.Length >= 2 && bytes[0] == 0xFF && bytes[1] == 0xFE)
        { //UTF-16 Little Endian BOM
            Console.WriteLine("文件编码为 UTF-16 Little Endian");
        }
        else if (bytes.Length >= 4 && bytes[0] == 0x00 && bytes[1] == 0x00 && bytes[2] == 0xFE && bytes[3] == 0xFF)
        {//UTF-32 Big Endian BOM
            Console.WriteLine("文件编码为 UTF-32 Big Endian");
        }
        else if (bytes.Length >= 4 && bytes[0] == 0xFF && bytes[1] == 0xFE && bytes[2] == 0x00 && bytes[3] == 0x00)
        {//UTF-32 Little Endian BOM
            Console.WriteLine("文件编码为 UTF-32 Little Endian");
        }
        else
        {// 默认按照当前系统的编码进行处理
            Console.WriteLine("未检测到 BOM,使用默认编码:" + Encoding.Default.EncodingName);
        }


        
        现在来回答前面的问题,为什么前面会有一个?号呢?
        
        因为在读取时用的是长度FileStream.Length属性
        
            它返回的值表示文件的大小(以字节为单位),而不是文件中实际内容的长度。该属
            性提供了一个方便的方式来获取文件的大小,包括文件的所有内容、占用的磁盘空间
            以及任何文件头或元数据。它通常用于确定文件大小,进行文件操作和内存分配等。
        
        查看一下这个文件是:带BOM的UTF8文本文件。下断点看一下:
      

 


        前三个字节是:0xEF,0xBB,0xBF,正是带BOM的UTF8的标志。
        
        此时最方便的方法就是利用StreamReader有一个自动判断BOM的方法。

        string s = @"E:\1.txt";
        using (StreamReader sr = new StreamReader(s, true))
        {
            Console.WriteLine(sr.ReadToEnd());
        }


        上面会自动检测是否有BOM而正确识别真实文件,而不会有?号出现。
        
        参数detectEncodingFromByteOrderMarks 设置为 true 时,StreamReader 将会根据文件
        的字节顺序标记(BOM)来确定文件的编码方式。如果文件存在 BOM,则它会自动使用正
        确的编码进行读取,跳过 BOM 部分,因此不会出现 “?” 号。
            
        当设置为 false 时,StreamReader 将不会依赖于字节顺序标记进行自动检测编码。但
        是,如果文件存在 BOM,这种情况下,StreamReader 仍然会正确地识别 BOM 并跳过它,
        然后使用正确的编码进行读取,因此不会出现 “?” 号。
        
        总结:无论 detectEncodingFromByteOrderMarks 参数设置为 true 还是 false,都会
            自动检测并跳过 BOM,并使用正确的编码进行读取,因此你不会看到 “?” 号的出现。
            
        既然都可以自动检测,那这个参数有屁用?
            参数的存在是为了处理一些特殊情况,例如当文件不带 BOM,或者当文件可能包含其
            他类型的编码时,比较自定义的BOM,通过设置该参数为 true 可以让 StreamReader 
            自动检测并选择正确的编码进行读取。比如检测出自定义的BOM格式。
    
        上面FileStream可以根据前面的字节来判断是否带BOM,并判断类型,虽然不是很准。
        
        下面用filsestream进行读取,因为知晓带BOM占三个字节,于是:

        string filePath = @"E:\1.txt";
        using (FileStream fsr = new FileStream(filePath, FileMode.Open, FileAccess.Read))
        {
            byte[] buffer = new byte[1024 * 8];
            fsr.Seek(3, SeekOrigin.Begin);//指定从开始后三个字才开始读
            int bytesRead = fsr.Read(buffer, 0, buffer.Length);
            Console.WriteLine(Encoding.UTF8.GetString(buffer, 0, bytesRead));
        }


        这样的结果,就再也没有?号了。
        
        fsr.Seek()用法: 
        long Seek (long offset, System.IO.SeekOrigin origin);
            offset 表示要移动的偏移量;
            origin 表示基于何处来计算偏移量。有三个选项:
                SeekOrigin.Begin:基于文件的起始位置进行偏移。
                SeekOrigin.Current:基于文件的当前位置进行偏移。
                SeekOrigin.End:基于文件的末尾位置进行偏移。
        Seek 方法会返回一个 long 类型的值,表示设置后文件流的新位置。
        

        using (FileStream fs = new FileStream("myfile.txt", FileMode.Open))
        {
            // 将文件流的当前位置设置为偏移量为100的位置,基于文件的起始位置
            fs.Seek(100, SeekOrigin.Begin);

            // 将文件流的当前位置向后偏移50个位置
            fs.Seek(50, SeekOrigin.Current);

            // 将文件流的当前位置设置为倒数第50个位置
            fs.Seek(-50, SeekOrigin.End);
        }    


    
        注意:调用一次 fs.Seek() 方法只会对当前的定位操作生效,并且对于后续的读取或写入
            操作,文件流会按照顺序继续定位。除非你再次调用 fs.Seek() 方法来更改当前的位置。
    
 


八、文本文件流(StreamReader与StreamWriter)


    
    1、StreamWriter(读取文本文件)
        
        Stream把所有内容当成二进制来看待,如果是文本内容,则需要程序员来处理文本和二进
        制之间的转换。
        
        用StreamWriter可以简化文本类型的Stream的处理
        
        StreamWriter是辅助Stream进行处理的。
        
        提示:StreamReader与StreamWriter直接处理的是字符,所以需要带上编码识别。
        

        using(StreamWriter writer = new StreamWriter(stream, encoding))
        {
            writer.WriteLine("你好");
        }


    
        常用的 StreamWriter 方法:

        (1)Write(string value):将指定的字符串写入流中。

                StreamWriter writer = new StreamWriter("myfile.txt");
                writer.Write("Hello, World!");


        (2)WriteLine(string value):将指定的字符串及后面换行符写入流中。

                StreamWriter writer = new StreamWriter("myfile.txt");
                writer.WriteLine("Hello, World!");


        (3)Write(char value):将指定的字符写入流中。

                StreamWriter writer = new StreamWriter("myfile.txt");
                writer.Write('A');


        (4)WriteLine(char value):将指定的字符及后面换行符写入流中。

                StreamWriter writer = new StreamWriter("myfile.txt");
                writer.WriteLine('A');


        (5)Write(char[] buffer):将字符数组中的内容写入流中。

                StreamWriter writer = new StreamWriter("myfile.txt");
                char[] buffer = { 'H', 'e', 'l', 'l', 'o' };
                writer.Write(buffer);


        (6)WriteLine(char[] buffer):将字符数组中的内容及后面换行符写入流中。

                StreamWriter writer = new StreamWriter("myfile.txt");
                char[] buffer = { 'H', 'e', 'l', 'l', 'o' };
                writer.WriteLine(buffer);


        (7)Flush():将缓冲区中的所有数据立即写入流中。

                StreamWriter writer = new StreamWriter("myfile.txt");
                writer.Write("Hello");
                // ...
                writer.Flush();


        (8)Close() 或 Dispose():关闭 StreamWriter 对象,并释放与其关联的资源。

                StreamWriter writer = new StreamWriter("myfile.txt");
                // ...
                writer.Close();
                // 或writer.Dispose();


        
        
    2、StreamReader类
    
        和StreamWriter类似,StreamReader简化了文本类型的流的读取
        

        Stream stream = File.OpenRead("E:/1.txt");//a
        using (StreamReader reader = new StreamReader(stream, Encoding.Default))
        {
            //Console.WriteLine(reader.ReadToEnd();
            Console.WriteLine(reader.ReadLine());
        }


        a的斜杠:
            在 C# 中,斜杠的方向在表达目录时通常没有区别。无论是使用 @"E:\1.txt" 还
        是 @"E:/1.txt",都可以表示相同的文件路径。这是因为在 Windows 和 Unix-like 
        系统中,都支持使用斜杠 / 或反斜杠 \ 作为文件路径的分隔符。
    
        ReadToEnd用于从当前位置一直读到最后,内容大的话会占内存;每次调用都往下走,
            不能无意中调用了两次。第二调用结果会为null,因为位置指针已经在末尾,
            向下读取为null。除非把位置指针重置到开头:

             reader.BaseStream.Position = 0;


        
        ReadLine读取一行,如果到了末尾,则返回null。注意中间无内容返回是""。
        
        问:下面结果是多少?a的asc是97,b为98

            string s = @"E:\1.txt";
            using (StreamReader sr = new StreamReader(s, Encoding.UTF8))
            {
                int n;
                while ((n = sr.Read()) > 0) ;
                {
                    Console.WriteLine(n);
                }
            }


        
        答:-1
        因为while后面是;号,实际上这个循环什么也没干,后面{}输出只能是跳出循环时的-1
        ,去掉while句后面;号,结果为97,98.
        
        提示:StreamReader.Read() 方法是以字符为单位进行读取,并返回表示字符的Unicode
            编码。而不是字节。当最后无字符时,返回-1.
    
        总结:相比于 FileStream,StreamReader 提供了更简化的读取接口、字符编码处理、
            自动资源释放和文本读取的便捷性。它适合于处理文本文件和简化读取操作,但
            对于需要高性能字节读取或底层文件操作的场景,FileStream 可能更为合适。
    
    
    
    3、练习
    
        案例:对职工工资文件处理,所有人的工资加倍然后输出到新文件。
        文件案例:
            马大哈|3000
            宋江|8000
        提示: (可以不参考提示。)
            先获得FileStream或者直接写文件路径(StreamReader(path))
                File.OpenRead(path);File.OpenWrite(path);
            再用FileStream构建一个StreamReader与StreamWriter
            如果不太会使用StreamReader和StreamWriter可以先
            用File.ReadAILines()和File.WriteAlILines()来做。
            

        string src = @"E:\1.txt";
        string des = @"E:\2.txt";
        using (StreamReader sr = new StreamReader(src, Encoding.UTF8))
        {
            using (StreamWriter sw = new StreamWriter(des, false, Encoding.UTF8))
            {
                string s;
                while ((s = sr.ReadLine()) != null)
                {
                    string[] s1 = s.Split(new char[] { '|' });
                    s1[1] = (Convert.ToInt32(s1[1]) * 2).ToString();
                    string s2 = string.Concat(s1[0], "|", s1[1]);
                    sw.WriteLine(s2);
                }
            }
        }


    
 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值