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