一、内存流
 
     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);
                }
            }
        } 
     
  



















