第15章 流与IO
15.1 .NET 流的架构
.NET 流的架构主要包含三个概念:** 后台存储 、 装饰器 以及 流适配器 **,如图所示:
C7.0 核心技术指南 第7版.pdf - p655 - C7.0 核心技术指南 第 7 版-P655-20240216192328
其中** 后台存储 和 装饰器 **为流。
- 后台存储流:负责处理 原始数据
- 装饰器流:可以透明地进行 二进制数据的转换 (例如加密)
- 适配器:提供了 处理更高级类型 (例如文本和 XML)的方法。
我们只需简单地将一个对象传人另一个对象的构造器就可以构成一个链条。
C7.0 核心技术指南 第7版.pdf - p655 - C7.0 核心技术指南 第 7 版-P655-20240216193328
15.2 使用流
15.2.1 读取和写入
Stream.Read
Stream.Read
方法用于将数据块读取到 byte
数组 中,并返回 接收的字节数 。返回值分两种情况:
- 返回值小于传入的 count 参数: 读取位置已达到流的末尾,或流本身是以小块方式提供数据(通常是网络流) 。
- 返回值等于传入的 count 参数: 数据块可能未读完 。
流的正确读取方式如下,该代码每次读取都判断读到的数据数量:
int bytesRead = 0;
int chunkSize = 1;
while (bytesRead < data.Length && chunkSize > 0)
{bytesRead += chunkSize = s.Read(data, bytesRead, data.Length - bytesRead);
}
C7.0 核心技术指南 第7版.pdf - p658 - C7.0 核心技术指南 第 7 版-P658-20240216201116
Stream.ReadByte
ReadByte
方法:它每次读取 一 个字节,通过 返回值 返回,并在流结束时 返回-1 。我们需要将得到的数据按照 byte
而非 int
进行处理。
Stream.Write
和 Stream.WriteByte
Write
方法和 WriteByte
方法将数据发送到流中。如果无法发送指定的字节,则 抛出异常 。
Notice
Read
和Write
方法中的offset
参数指的是buffer
数组中开始读写的索引位置,而不是流中的位置。
15.2.2 查找
流的 CanSeek
属性为 true
才能进行查找。若流可查找,则:
-
流的 长度(Length) 可修改
通过
SetLength
方法设置
SetLength
的使用场景如下:- 文件截断或扩展:当使用文件流(
FileStream
)时,SetLength
方法可以用来截断或扩展文件。如果指定的长度小于当前文件大小,文件将被截断,超出的数据会被丢弃。如果指定的长度大于当前文件大小,文件将被扩展,新增的部分通常会用零字节填充。 - 调整内存流的大小:对于
MemoryStream
,SetLength
方法可以用来调整内存中存储的数据量。这可以在你需要更大或更小的缓冲区时非常有用。
- 文件截断或扩展:当使用文件流(
-
Position
属性可修改可以改变读写位置
-
Seek
方法可以参照当前位置(SeekOrigin.Current
)
如果流不支持查找功能(例如加密流),则只能通过 遍历整个流 获取长度。重新读取先前的位置也必须 关闭整个流,再从头读取 。
15.2.3 关闭和刷新
通常,流对象的标准销毁语义为:
-
Dispose
和Close
方法的功能 是一样的 。 - 重复销毁或者关闭流对象 不会产生任何错误 。
Flush
方法可以强制将缓冲区数据写入后台存储中。当流关闭的时候,也会自动调用 Flush
方法。因此关闭前无需再调用 Flush
方法:
// 没有必要调用 s.Flush()
s.Flush();
s.Close();
15.2.4 超时
相关的属性有:
-
CanTimeout
-
ReadTimeout
-
WriteTimeout
网络流支持该特性,文件流和内存流不支持。设置的超时时间以 毫秒 为单位, 0 代表不进行超时设置。
15.2.5 线程安全
通常情况下流并不是线程安全的。Stream
类提供了一个静态的 Synchronized()
方法,该方法可以接受任何类型的流,并返回一个线程安全的包装器,这个包装器会使用一个排它锁保证每一次读、写或者查找操作只能有一个线程执行。
15.2.6 后台存储流
如下为主要的后台存储流。此外,有 Stream.Null
静态字段,用于表示“空”流,常用于单元测试。
15.2.7 FileStream
类
15.2.7.1 创建 FileStream
实例化 FileStream
有两种方式:
- 通过
File
类型的静态方法 - 通过
FileStream
的构造器
静态方法
File.ReadLines
和File.ReadAllLines
类似,但前者会返回一个延迟加载的IEnumerable<string>
类型。它无须将所有内容加载到内存中,因而更加高效。同时它适合与 LINQ 结合使用。
15.2.7.2 指定文件名
Environment.CurrentDirectory
属性和 AppDomain.CurrentDomain.BaseDirectory
属性的区别:
-
Environment.CurrentDirectory
: 程序执行 路径 -
AppDomain.CurrentDomain.BaseDirectory
: 可执行文件所在 路径
C7.0 核心技术指南 第7版.pdf - p662 - C7.0 核心技术指南 第 7 版-P662-20240217173752
- 用户操作:用户可以通过命令行改变工作目录,然后从那个目录启动应用程序,此时应用程序的当前工作目录就是用户指定的目录,而不是应用程序本身的目录。
- 快捷方式设置:通过快捷方式启动应用程序时,快捷方式的属性可以指定“开始于”(或“工作目录”)的位置,这会影响应用程序的当前工作目录。
- 程序代码:应用程序在运行时可以通过代码改变自己的当前工作目录。
因此,建议使用 AppDomain.CurrentDomain.BaseDirectory
获取程序目录:
string baseFolder = AppDomain.CurrentDomain.BaseDirectory;
string logoPath = Path.Combine (baseFolder,"logo.jpg");
Console.WriteLine (File.Exists (logoPath))j
15.2.7.3 FileMode
和 FileAccess
FileMode
指示如何 处理文件 ,FileAccess
指示如何 操作流 。
FileMode
的成员为:
public enum FileMode
{CreateNew = 1,Create,Open,OpenOrCreate,Truncate,Append
}
FileAccess
的成员为:
[Flags]
public enum FileAccess
{Read = 1,Write = 2,ReadWrite = 3
}
FileMode
每个成员对应的静态方法有:
C7.0 核心技术指南 第7版.pdf - p663 - C7.0 核心技术指南 第 7 版-P663-20240217174507
FileMode
和 FileAccess
相组合又对应了其他静态方法,如下代码相当于 File.OpenRead
方法:
using (var fs=new FileStream("x.bin", FileMode.Open, FileAccess.Read))...
15.2.7.4 FileStream
的高级特性
以下是创建 FileStream
时可选的其他参数:
-
FileShare
枚举:占用文件后,若其他进程访问该文件,通过该枚举可以给予一定的访问权限(None
、Read
、ReadWrite
或者Write
,其中Read
为默认权限) -
内部缓冲区的大小(字节为单位,默认大小为 4KB)。
-
FileSecurity
对象:描述给新文件分配的用户角色和权限。 -
FileOptions
标记枚举,其中包括:-
Encrypted
:请求操作系统加密。 -
DeleteOnClose
:在文件关闭时自动删除临时文件。 -
RandomAccess
和SequentialScan
:优化提示。 -
WriteThrough
:要求操作系统禁用写后缓存,适用于事物文件或日志文件。
-
使用 FileShare.ReadWrite
打开一个文件可以允许其他进程或用户读写同一个文件。为了避免混乱,我们可以使用以下方法在读或者写之前锁定文件的特定部分。
// Defined on the FileStream class:
public virtual void Lock (long position, long length);
public virtual void Unlock (long position, long length);
如果所请求的文件段的部分或者全部已经被锁定,Lock 操作会抛出异常。
15.2.8 MemoryStream
MemoryStream
使用 数组 作为后台存储,可以通过 CopyTo
方法将数据复制到 MemoryStream
中:
var ms = new MemoryStream();
sourceStream.CopyTo(ms);
获取 MemoryStream
中的数据方式有二:
-
ToArray
方法返回数据对应的
byte
数组。 -
GetBuffer
返回底层存储数组的引用,比流的实际长度要长。
Tips
MemoryStream
的关闭(Close
)和刷新(Flush
)不是必须的。MemoryStream
关闭后将无法再次读写,但是我们仍然可以调用ToArray
方法来获得底层的数据。而刷新操作则不会对内存流执行任何操作。
15.2.9 PipeStream
管道类型有两种:
- ** 匿名 管道(速度更快):支持同一台计算机中的父进程和子进程之间进行 单 向**通信。
- ** 命名 管道(更加灵活):支持同一台或不同计算机(使用 Windows 网络)的任意两个进程间进行 双 向**通信。
管道很适合在同一台计算机进行进程间通信(IPC):它不依赖于任何网络传输(因此没有网络协议开销),性能更好,也不会有防火墙问题。
PipeStream
是抽象类,有 4 个子类:
- 匿名管道:
AnonymousPipeServerStream
和AnonymousPipeClientStream
- 命名管道:
NamedPipeServerStream
和NamedPipeClientStream
15.2.9.1 命名管道
如下是命名管道的简单使用:
using(var s = new NamedPipeServerStream("pipedream"))
{s.WaitForConnection();s.WriteByte(100);Console.WriteLine(s.ReadByte());
}
using (var s = new NamedPipeClientStream("pipedream"))
{s.Connect();Console.WriteLine(s.ReadByte());s.WriteByte(200);
}
命名管道流默认为双向通信,但需要注意:双方不能同时发送消息,也不能同时接收消息。
管道的消息传输模式
命名 管道的 Message 模式支持通过 IsMessageComplete
属性确定是否完整的读取了消息,其使用方式如下:
static byte[] ReadMessage(PipeStream s)
{MemoryStream ms = new MemoryStream();var buffer = new byte[0x1000]; // 读取4KB块do{ms.Write(buffer, 0, s.Read(buffer, 0, buffer.Length));}while (!s.IsMessageComplete);return ms.ToArray();
}
基于该方法,以下是消息传输模式的示例代码:
using(var s = new NamedPipeServerStream("pipedream", PipeDirection.InOut, 1, PipeTransmissionMode.Message)){s.WaitForConnection();var msg = Encoding.UTF8.GetBytes("Hello");s.Write(msg, 0, msg.Length);Console.WriteLine(Encoding.UTF8.GetString(ReadMessage(s)));
}
using (var s = new NamedPipeClientStream("pipedream"))
{s.Connect();s.ReadMode = PipeTransmissionMode.Message;Console.WriteLine(Encoding.UTF8.GetString(ReadMessage(s)));var msg = Encoding.UTF8.GetBytes("Hello right back!");s.Write(msg, 0, msg.Length);
}
15.2.9.2 匿名管道
匿名管道也分为客户端、服务端,它具有如下特点:
-
仅支持 单 向通讯, 双 向通讯需 定义两个管道
实例化时接受
PipeDirection
枚举的In
或Out
作为参数确定通讯方向,不支持InOut
。 -
通过
GetClientHandleAsString
方法获取句柄信息,通过该信息客户端进行连接。 -
仅支持
PipeTransmissionMode
的 Byte 模式,不支持 Message 模式。
匿名管道使用方式如下:
string clientExe = @"d:\PipeDemo\ClientDemo.exe";
var inherit = HandleInheritability.Inheritable;
using (var tx = new AnonymousPipeServerStream(PipeDirection.Out, inherit))
using (var rx = new AnonymousPipeServerStream(PipeDirection.In, inherit))
{var txID = tx.GetClientHandleAsString();var rxID = rx.GetClientHandleAsString();var startInfo = new ProcessStartInfo(clientExe, txID + " " + rxID);startInfo.UseShellExecute = false; // 要求作为子进程var p = Process.Start(startInfo);// 释放句柄资源,该句柄用于生成ID。连接完成之前不可释放。tx.DisposeLocalCopyOfClientHandle();rx.DisposeLocalCopyOfClientHandle();tx.WriteByte(100);Console.WriteLine("Server received: " + rx.ReadByte());p.WaitForExit();
}
string rxID = args[0];
string txID = args[1];using (var rx = new AnonymousPipeClientStream(PipeDirection.In, rxID))
using (var tx = new AnonymousPipeClientStream(PipeDirection.Out, txID))
{Console.WriteLine("Client received: " + rx.ReadByte());tx.WriteByte(200);
}
Suggest
与命名管道一样,客户端和服务器必须协调它们的发送和接收,并且统一每一次传输的数据长度。但是,匿名管道不支持消息模式,因此必须实现自已的消息长度协议。一种方法是在每一次传输的前四个字节中发送一个整数值,来定义后续消息的长度。
BitConverter
类可以在整数和含四个元素的字节数组间进行转换。
装饰器流
下图为所有装饰器流的类型:
C7.0 核心技术指南 第7版.pdf - p669 - C7.0 核心技术指南 第 7 版-P669-20240219125429
15.2.10 BufferedStream
BufferedStream
为装饰器,用于 提供缓冲 / 扩充缓冲区 。
如下代码对 FileStream
进行包装,将缓冲区 扩充至 20KB :
const string Filename = "MyFile.bin";
File.WriteAllBytes(Filename, new byte[100_000]);
using(FileStream fs = File.OpenRead(Filename))
using(BufferedStream bs = new BufferedStream(fs, 20_000)) // 20K缓冲
{bs.ReadByte();Console.WriteLine(fs.Position); // 20000
}
这段代码虽然只读了一个字节,但底层流已经读了 20k 字节,剩余的 19999 次 ReadByte
调用将不再访问 FileStream
。
15.3 流适配器
Stream
仅支持处理 字节 ,一些类提供了高级的处理方式,具体如下:
-
文本适配器
-
TextReader
、TextWriter
:抽象类 -
StreamReader
、StreamWriter
-
StringReader
、StringWriter
-
-
二进制适配器
-
BinaryReader
、BinaryWriter
-
-
XML 适配器
-
XmlReader
、XmlWriter
-
15.3.1 文本适配器
TextReader
和 TextWriter
为 抽象 类,它有两个实现:
-
StreamReader
/StreamWriter
使用
Stream
存储其原始数据,将流的字节转换为字符或者字符串。 -
StringReader
/StringWriter
使用内存字符串(实际是
StringBuilder
)实现了TextReader
/TextWriter
.
15.3.1.1 StreamReader
和 StreamWriter
File
类提供了一些静态方法,返回此类型,如:
-
返回
StreamWriter
-
File.CreateText
-
File.AppendText
-
-
返回
StreamReader
-
File.OpenText
-
const string Path = "test.txt";
using (TextWriter writer = File.CreateText(Path))
{writer.WriteLine("Line1");writer.WriteLine("Line2");
}using (TextWriter writer = File.AppendText(Path))writer.WriteLine("Line3");
using (TextReader reader = File.OpenText(Path))while(reader.Peek() > -1)Console.WriteLine(reader.ReadLine());
// or
using (TextReader reader = File.OpenText(Path))
{string content;while ((content = reader.ReadLine()) != null){Console.WriteLine(content);}
}
此外还可以通过构造器创建实例,其构造器接受 Stream
实例或** 文件 **。
15.3.1.2 字符编码
StreamReader
和 StreamWriter
默认使用 UTF-8 编码
C# 的 char
使用 2 byte 表示,便于跳转到流中特定字符上,刚好对应 UTF-16 编码。UTF-16 使用 2 byte 前缀来表明字节顺序(“小字节序”或者“大字节序”,即最低有效字节在前还是最高有效字节在前)。Windows 系统采用的默认标准是小字节序。更多内容见 Unicode 编码
C7.0 核心技术指南 第7版.pdf - p673 - C7.0 核心技术指南 第 7 版-P673-20240219173405
15.3.1.3 StringReader
和 StringWriter
StringReader
和 StringWriter
用于将 字符串 包装为 流 ,便于一些仅接受流的方法使用。例如:
XmlReader r = XmlReader.Create(new StringReader(myString))
15.3.2 二进制适配器 BinaryReader
& BinaryWriter
BinaryReader
和 BinaryWriter
能够读写:
- 基本的数据类型
-
string
- 基础数据类型的数组
如下代码演示了二进制数据的读写:
public class Person
{public string Name;public int Age;public double Height;
}
public void SaveData(Stream s)
{var w = new BinaryWriter(s);w.Write(Name);w.Write(Age);w.Write(Height);w.Flush();
}
public void LoadData(Stream s)
{var r = new BinaryReader(s);Name = r.ReadString();Age = r.ReadInt32();Height = r.ReadDouble();
}
BinaryReader
也可以读取 byte 流:
byte[] data = new BinaryReader(stream).ReadBytes((int)stream.Length);
15.3.2.1 BinaryReader.ReadString
和 BinaryWriter.Write(string value)
BinaryWriter.Write(string value)
方法写入字符串数据时十分特别,它会用“7 位编码的整数”添加一个前缀,标明字符串数据的长度。
该 Write
方法搭配 BinaryReader.ReadString
一起使用,可以做到写多少,读多少:
using (TcpClient client = new TcpClient ("localhost", 51111)) using (NetworkStream n = client.GetStream()) {BinaryWriter w = new BinaryWriter (n);w.Write ("Hello");w.Flush();Console.WriteLine (new BinaryReader (n).ReadString()); }
TcpListener listener = new TcpListener (IPAddress.Any, 51111); listener.Start(); using (TcpClient c = listener.AcceptTcpClient()) using (NetworkStream n = c.GetStream()) {string msg = new BinaryReader (n).ReadString();BinaryWriter w = new BinaryWriter (n);w.Write (msg + " right back!");w.Flush(); // 从此未释放 Writer, } // 因此必须调用 Flush 方法 listener.Stop();
Info
7 位编码的整数(Varint Encoding)
在这种编码方式中,每个字节的最高位(第 8 位)用作“继续位”(continuation bit),指示后续字节是否也是整数的一部分。剩下的 7 位用于表示实际的整数值。因此,这种方式可以用变长的字节数来表示整数:
- 如果整数小于 128(即 0x80),则只需要 1 个字节。
- 如果整数大于等于 128,则需要多个字节,直到所有的 7 位块都编码完毕。
具体示例
假设我们要编码一个整数 300:
- 将 300 转换为二进制表示:1_0010_1100
- 将其分成 7 位的块:001_0110 和 000_0010
对每个块添加最高位(继续位):
- 第一块 0010110 需要继续,变为 1010110(0xB6)
- 第二块 0000010 是最后一块,变为 0000010(0x02)
因此,整数 300 用两个字节表示:0xB6 0x02
注意
7 位编码的整数采用小端字节排序,低位在前,高位在后。
15.3.3 关闭和销毁“流适配器”
关闭适配器会 自动关闭 底层流。using 语句是由内向外销毁,因此适配器先关闭,流后关闭。即使适配器的 构造器抛出异常 ,底层流仍会关闭。因此嵌入 using 语句是最佳的选择。例如,下代码先释放 writer
,再释放 fs
:
using (FileStream fs = File.Create("text.txt"))
using (TextWriter writer = new StreamWriter(fs))writer.WriteLine("Line");
Warn
上述代码,切勿先关闭
FileStream
,再关闭TextWriter
,这可能导致writer
中缓存待写的数据未及时写入!C7.0 核心技术指南 第7版.pdf - p677 - C7.0 核心技术指南 第 7 版-P677-20240220122916
不释放底层流的适配器
StreamReader
/ StreamWriter
加入了一个新的构造器,保证流在适配器销毁之后仍然保持打开的状态。如下两段代码等价:
using (FileStream fs = new FileStream ("test.txt", FileMode.Create))
{StreamWriter writer = new StreamWriter(fs);writer.WriteLine("Hello");writer.Flush();fs.Position = 0;Console.WriteLine(fs.ReadByte());
}
using (var fs = new FileStream("test.txt", FileMode.Create))
{using (var writer = new StreamWriter(fs, new UTF8Encoding(false, true), 0x400, true))writer.WriteLine("Hello");fs.Position = 0;Console.WriteLine(fs.ReadByte());Console.WriteLine(fs.Length);
}
Summary
包括内存数据压缩中提到的
DeflateStream
,有三个流支持 Close 后底层流仍保持打开状态
15.4 压缩流
System.IO.Compression
命名空间有两个通用的压缩流,为 装饰 器,支持 ZIP 压缩算法:
-
DeflateStream
-
GZipStream
- 会在文件开头和结尾处写入额外的协议信息,其中包括检测错误的 CRC。
- 遵循公认标准 RFC 1952。
使用方式如下:
using (Stream s = File.Create("compressed.bin"))
using (Stream ds = new DeflateStream(s, CompressionMode.Compress)){for(byte i = 0; i < 100; i++)ds.WriteByte(i);
}
using (Stream s = File.OpenRead("compressed.bin"))
using (Stream ds = new DeflateStream(s, CompressionMode.Decompress)){for (byte i = 0; i < 100; i++)Console.WriteLine(ds.ReadByte());
}
内存数据压缩
如下代码在内存中压缩数据:
byte[] data = new byte[1000];var ms = new MemoryStream();
using (Stream ds = new DeflateStream(ms, CompressionMode.Compress))ds.Write(data, 0, data.Length);byte[] compressed = ms.ToArray();
Console.WriteLine(compressed.Length); // 压缩后数据仅 11 bytems = new MemoryStream(compressed);
using (Stream ds = new DeflateStream(ms, CompressionMode.Decompress))for (int i = 0; i < 1000; i += ds.Read(data, i, 1000 - i));
DeflateStream
构造器支持 Close 时不关闭底层流,用法如下:
byte[] data = new byte[1000];
MemoryStream ms = new MemoryStream();
using (Stream ds = new DeflateStream(ms, CompressionMode.Compress, true))await ds.WriteAsync(data, 0, data.Length);
Console.WriteLine(ms.Length);// 因流未关闭,可以继续使用
ms.Position = 0;
using (Stream ds = new DeflateStream(ms, CompressionMode.Decompress))for(int i = 0; i < 1000; i += await ds.ReadAsync(data, i, 1000 - i));
15.5 操作 ZIP 文件
ZipArchive
和 ZipFile
用于 ZIP 压缩
-
ZipArchive
:用于操作流 -
ZipFile
:静态类,辅助ZipArchive
进行操作
该类操作时与压缩软件别无二致,可以做到:
-
压缩
ZipFile.CreateFromDirectory (@"d:\MyFolder,@"d:\compressed.zip");
-
解压
ZipFile.ExtractToDirectory (@"d:\compressed.zip", @"d:\MyFolder");
-
速度优先还是体积优先等。
-
是否包含源目录名称
-
读写文件并遍历
ZipFile.Open
方法、ZipFile.Entries
属性using(ZipArchive zip = ZipFile.Open (@"d:\zz.zip",ZipArchiveMode.Read))foreach (ZipArchiveEntry entry in zip.Entries)Console.WriteLine (entry.FullName + " " + entry.Length);
-
向压缩包中添加文件,
byte[] data = File.ReadAllBytes (@"d:\foo.dll"); using(ZipArchive zip = ZipFile.Open(@"d:\zz.zip", ZipArchiveMode.Update))zip.CreateEntry(@"bin\x64\foo.dl1").Open().Write(data,O,data.Length);
-
删除压缩包文件
ZipArchiveEntry.Delete
方法 -
加压指定文件
ZipFileExtensions.ExtractToFile
方法
若使用 MemoryStream
创建 ZipArchive
,可以完全在内存中进行操作。
15.6 文件与目录操作
System.IO
对文件、目录操作的接口有:
- 静态类:
File
和Directory
- 实例方法:
FileInfo
和DirectoryInfo
静态类 Path
:用于处理文件名称或者目录路径字符串。同时 Path 还可以用于临时文件的处理。
15.6.1 File 类
15.6.1.1 压缩与加密属性
本节讲解文件属性中的“压缩”和“加密”选项:
15.6.1.2 文件安全性
本节讲解权限控制信息:
15.6.1 File
类
C7.0 核心技术指南 第7版.pdf - p681 - C7.0 核心技术指南 第 7 版-P681-20240221123020
15.6.2 Directory
类
C7.0 核心技术指南 第7版.pdf - p684 - C7.0 核心技术指南 第 7 版-P684-20240221123050
C7.0 核心技术指南 第7版.pdf - p685 - C7.0 核心技术指南 第 7 版-P685-20240221123119
15.6.3 FileInfo
类和 DirectoryInfo
类
File
和 Directory
适用于 操作文件或目录单次 。FileInfo
和 DirectoryInfo
适用于 单个项目进行一系列调用 。
FileInfo
类以实例成员的形式提供了 File
类型静态方法的大部分功能。此外还包含一些额外的属性,如 Extensions
、Length
、IsReadOnly
以及 Directory
(返回一个 DirectoryInfo
对象)。
15.6.4 Path
类型
静态类 Path
中的方法和字段可用于处理路径和文件名称。
假设有如下路径:
string dir = @"c:\mydir";
string file = "myfile.txt";
string path = @"c:\mydir\myfile.txt";
则有:
表达式 | 结果 |
---|---|
Directory.GetCurrentDirectory() |
k:\demo| |
Path.IsPathRooted (file) |
False |
Path.IsPathRooted (path) |
True |
Path.GetPathRoot (path) |
*c:* |
Path.GetDirectoryName (path) |
c:\mydir |
Path.GetFileName (path) |
myfile.txt |
Path.GetFullPath (file) |
k:\demo\myfile.txt |
Path.Combine (dir, file) |
c:\mydir\myfile.txt |
文件扩展名 | |
Path.HasExtension (file) |
True |
Path.GetExtension (file) |
.txt |
Path.GetFileNameWithoutExtension (file) |
myfile |
Path.ChangeExtension (file, ".log") |
myfile.log |
分隔符和字符 | |
Path.AltDirectorySeparatorChar |
/ |
Path.PathSeparator |
; |
Path.VolumeSeparatorChar(卷分隔符) | : |
Path.GetInvalidPathChars() |
chars 0 to 31 and "<>| |
Path.GetInvalidFileNameChars() |
chars 0 to 31 and "<>|:*?\/ |
文件 | |
Path.GetTempPath() |
|
Path.GetRandomFileName() |
d2dwuzjf.dnp |
Path.GetTempFileName() |
GetRandomFileName
方法会返回一个完全唯一的 8.3 格式的文件名,但不会创建文件。
GetTempFileName
会使用一个自增计数器生成一个临时文件(这个计数器每隔 65000 次重复一遍),并用这个名称在本地临时目录下创建一个 0 字节的文件。
C7.0 核心技术指南 第7版.pdf - p687 - C7.0 核心技术指南 第 7 版-P687-20240221131340
15.6.5 特殊文件夹
Enviroment.GetFolderPath
可以获取特殊功能的文件夹(如 MyDocument、Program Files、Application Data 等)。
Environment.SpecialFolder
为枚举类型,包含了 Windows 中所有的特殊目录:
C7.0 核心技术指南 第7版.pdf - p688 - C7.0 核心技术指南 第 7 版-P688-20240221131732
C7.0 核心技术指南 第7版.pdf - p688 - C7.0 核心技术指南 第 7 版-P688-20240221131806
应用程序数据存储位置的选择
-
ApplicationData
- 用途:用于存储当前用户的应用程序数据,这些数据可以在用户的所有设备之间漫游(如果支持漫游用户配置文件的话)。通常用于存储配置文件、用户偏好设置和非临时数据。
- 路径示例:通常位于 C:\Users[用户名]\AppData\Roaming\ 下
-
LocalApplicationData
- 用途:用于存储特定于本地机器的应用程序数据。这些数据不会随用户的漫游配置文件在不同的机器之间漫游。适用于大型数据文件或机器特定的信息,例如缓存文件。
- 路径示例:通常位于 C:\Users[用户名]\AppData\Local\ 下
-
CommonApplicationData
- 用途:用于存储所有用户共享的应用程序数据,如应用程序级的配置文件、帮助文件。这些数据对计算机上的所有用户可见且共享。
- 路径示例:通常位于 C:\ProgramData\ 下
具体区别详见 ApplicationData、LocalApplicationData 和 CommonApplicationData 区别
C7.0 核心技术指南 第7版.pdf - p689 - C7.0 核心技术指南 第 7 版-P689-20240221171951
15.6.6 查询卷信息
我们可以使用 DriveInfo
类来查询计算机驱动器相关的信息:
DriveInfo c = new DriveInfo ("c"); // Query the C:drive.
long totalsize = c.TotalSize; //Size in bytes.
long freeBytes = c.TotalFreeSpace; // Ignores disk quotas.
long freeToMe = c.AvailableFreeSpace; //Takes quotas into account.
foreach (DriveInfo d in DriveInfo.GetDrives())//All defined drives.
{Console.WriteLine(d.Name); //C:Console.WriteLine(d.DriveType); //FixedConsole.WriteLine(d.RootDirectory);//C:\if (d.IsReady) //If the drive is not ready, the following two properties will throw exceptions:{Console.WriteLine(d.VolumeLabel);//The Sea DriveConsole.WriteLine(d.DriveFormat);//NTFS}
}
静态方法 GetDrives
会返回所有映射的驱动器,包括 CD-ROM、内存卡和网络连接。
DriveType
是一个枚举类型,它包括如下值:
Unknown
, NoRootDirectory
, Removable
, Fixed
, Network
, CDRom
, Ram
15.6.7 捕获文件系统事件
FileSystemWatcher
类可以监控一个目录(或者子目录)的活动,包括:创建、修改、重命名、删除文件或子目录,更改其属性。活动会触发 FileSystemWatch
类的事件。例如:
static void Main()
{Watch(@"D:\Temp", "*.txt", true);Thread.Sleep(100000);
}
static void Watch(string path, string filter, bool includeSubDirs)
{using (var watcher = new FileSystemWatcher(path, filter)){watcher.Created += FileCreatedChangedDeleted;watcher.Changed += FileCreatedChangedDeleted;watcher.Deleted += FileCreatedChangedDeleted;watcher.Renamed += FileRenamed;watcher.Error += FileError;watcher.IncludeSubdirectories = includeSubDirs;watcher.EnableRaisingEvents = true;Console.WriteLine("Listening for events press <enter>to end");Console.ReadLine();}
}
static void FileCreatedChangedDeleted (object o, FileSystemEventArgs e) => Console.WriteLine("File {o} has been {1}", e.FullPath, e.ChangeType);
static void FileRenamed (object o,RenamedEventArgs e)=> Console.WriteLine("Renamed:{o}->{1}", e.OldFullPath, e.FullPath);
static void FileError (object o, ErrorEventArgs e)=> Console.WriteLine ("Error::" + e.GetException().Message);
C7.0 核心技术指南 第7版.pdf - p691 - C7.0 核心技术指南 第 7 版-P691-20240221174654
C7.0 核心技术指南 第7版.pdf - p691 - C7.0 核心技术指南 第 7 版-P691-20240221174851
15.7 在 UWP 中进行文件 I/O 操作
C7.0 核心技术指南 第7版.pdf - p691 - C7.0 核心技术指南 第 7 版-P691-20240221175146
15.7.1 操作目录
15.7.2 操作文件
15.7.3 UWP 应用的独立存储区
15.8 内存映射文件
内存映射文件提供了两个主要特性:
- 高效地随机访问文件中的数据
- 在同一台计算机的不同进程间共享内存
15.8.1 内存映射文件和随机 I/O
MemoryMappedFile
将文件读取至内存中,因此有更好的 随机 访问性能。FileStream
和内存映射文件的速度有如下关系:
-
FileStream
的顺序 I/O 速度比MemoryMappedFile
快 10 倍。 -
MemoryMappedFile
的随机 I/O 速度比FileStream
快 10 倍。
内存映射文件的使用方式如下:
// 创建文件,用于后续使用
File.WriteAllBytes("long.bin", new byte[1_000_000]);
// 通过流/文件实例化MemoryMappedFile
using(MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile("long.bin"))
// 通过 MemoryMappedViewAccessor 读写内存
using(MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor()){accessor.Write(500_000, (byte) 77);Console.WriteLine(accessor.ReadByte(500_000));
}
关于 MemoryMappedViewAccessor
,详见 15.8.3 使用视图访问器 MemoryMappedViewAccessor
15.8.2 内存映射文件和共享内存
内存映射文件可以被视为“内存中的文件”,不同进程可以访问同一“文件”,方式如下:
- 一个进程调用
MemoryMappedFile.CreateNew
创建共享内存。 - 另一个进程调用
MemoryMappedFile.OpenExisting
共享内存。
用例如下:
using (MemoryMappedFile mmFile = MemoryMappedFile.CreateNew("Demo", 500))
using (MemoryMappedViewAccessor accessor = mmFile.CreateViewAccessor())
{accessor.Write(0, 12345);Console.ReadLine(); // 保活
}
// This can run in a separate EXE:
using (MemoryMappedFile mmFile = MemoryMappedFile.OpenExisting("Demo"))
using (MemoryMappedViewAccessor accessor = mmFile.CreateViewAccessor())Console.WriteLine(accessor.ReadInt32(0)); //12345
15.8.3 使用视图访问器 MemoryMappedViewAccessor
MemoryMappedViewAccessor
用于在指定位置读写值。非托管内存仅支持非托管数据,因此读写仅支持 值 类型数据及 数组 。若要写入托管数据,需要将数据映射为 字节数组 :
byte[] data = Encoding.UTF8.GetBytes("This is a test");
accessor.Write(0, data.Length);
accessor.WriteArray(4, data, 0, data.Length);
byte[] data = new byte[accessor.ReadInt32(0)];
accessor.ReadArray(4, data, 0, data.Length);
Console.WriteLine(Encoding.UTF8.GetString(data));
下面的例子将值类型数据(struct)写入内存:
struct Data { public int X, Y; }var value = new Data{ X = 123, Y = 456 };
accessor.Write(0, ref value);
accessor.Read(0, out value);
value.Dump();
更快的访问方式是通过指针直接访问内存:
unsafe
{byte* pointer = null;try{accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer);int* intPointer = (int*) pointer;Console.WriteLine(*intPointer);}finally{if(pointer != null)accessor.SafeMemoryMappedViewHandle.ReleasePointer();}
}
Tips
指针的性能优势在处理大型结构时会更加凸显。因为它可以直接处理原始数据,而不是通过
Read
和Write
方法在托管和非托管内存间拷贝数据。我们将在第 25 章内存的分配与使用详细介绍相关内容。
15.9 独立存储区域 IsolatedStorageFileStream
每一个.NET 应用程序都可以访问其独有的本地存储区域,称为独立存储区(isolated storage)。如果应用程序无法访问标准文件系统(因此也无法在 ApplicationData、LocalApplicationData、CommonApplicationData、MyDocuments 中写入数据)那么则更适合使用独立存储区。使用受限 Internet 权限部署的 Silverlight 应用程序和 ClickOnce 应用程序就属于这种情况。