第15章 流与IO
第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 byte
ms = 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() |
\Temp |
Path.GetRandomFileName() |
d2dwuzjf.dnp |
Path.GetTempFileName() |
\Temp\tmp14B.tmp |
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); //Fixed
Console.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 Drive
Console.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 应用程序就属于这种情况。
第15章 流与IO的更多相关文章
- Linux就这个范儿 第15章 七种武器 linux 同步IO: sync、fsync与fdatasync Linux中的内存大页面huge page/large page David Cutler Linux读写内存数据的三种方式
Linux就这个范儿 第15章 七种武器 linux 同步IO: sync.fsync与fdatasync Linux中的内存大页面huge page/large page David Cut ...
- unix network programming(3rd)Vol.1 [第13~15章]《读书笔记系列》
第13章 守护进程和inetd 超级服务器 syslog() daemon_init() setuid() setgid() 第14章 高级IO 标准I/O函数库,支持3种缓冲 缓冲(读写存储设备(硬 ...
- 【机器学习实战】第15章 大数据与MapReduce
第15章 大数据与MapReduce 大数据 概述 大数据: 收集到的数据已经远远超出了我们的处理能力. 大数据 场景 假如你为一家网络购物商店工作,很多用户访问该网站,其中有些人会购买商品,有些人则 ...
- 【RL-TCPnet网络教程】第15章 RL-TCPnet之创建多个TCP连接
第15章 RL-TCPnet之创建多个TCP连接 本章节为大家讲解RL-TCPnet的TCP多客户端实现,因为多客户端在实际项目中用到的地方还挺多,所以我们也专门开启一个章节做讲解.另外,学习 ...
- 第15章 上下文管理器和else块
#<流流畅的Python>第15章 上下文管理器和else块 #15.1 先做这个,再做那个:if语句之外的else块 #else子句不仅能在if语句中使用,还能在for.while和tr ...
- 第15章 RCC—使用HSE/HSI配置时钟
第15章 RCC—使用HSE/HSI配置时钟 全套200集视频教程和1000页PDF教程请到秉火论坛下载:www.firebbs.cn 野火视频教程优酷观看网址:http://i.youku. ...
- 第15章 RCC—使用HSE/HSI配置时钟—零死角玩转STM32-F429系列
第15章 RCC—使用HSE/HSI配置时钟 全套200集视频教程和1000页PDF教程请到秉火论坛下载:www.firebbs.cn 野火视频教程优酷观看网址:http://i.youku. ...
- 【STM32H7教程】第15章 STM32H7的GPIO基础知识(重要)
完整教程下载地址:http://www.armbbs.cn/forum.php?mod=viewthread&tid=86980 第15章 STM32H7的GPIO基础知识(重要) ...
- 20190827 On Java8 第十四章 流式编程
第十四章 流式编程 流的一个核心好处是,它使得程序更加短小并且更易理解.当 Lambda 表达式和方法引用(method references)和流一起使用的时候会让人感觉自成一体.流使得 Java ...
- Java面向对象程序设计第14章3-8和第15章6
Java面向对象程序设计第14章3-8和第15章6 3.完成下面方法中的代码,要求建立一个缓冲区,将字节输入流中的内容转为字符串. import java.io.*; public class tes ...
随机推荐
- 2025年前端面试准备css篇
1.css 盒子模型 css包含了内容(content) ,内边距(padding),边框(border),外边距(margin) 等因素. css 标准盒子模型宽包括:margin+border+p ...
- dotnet学习笔记-专题04-配置的读取和写入-01
配置的读取和写入 读取配置的类,包括手动从json中读取配置.将json配置与配置类绑定.从控制台读取配置.从环境变量读取配置 using System.Diagnostics; using Micr ...
- ScheduledThreadPoolExecutor与System#nanoTime
一直流传着Timer使用的是绝对时间,ScheduledThreadPoolExecutor使用的是相对时间,那么ScheduledThreadPoolExecutor是如何实现相对时间的? 先看看S ...
- Chrome插件之油猴(详尽版本)
官方文档: https://www.tampermonkey.net/documentation.php#google_vignette 1.注释语法: // @match https://passp ...
- golang之json.RawMessage
RawMessage 具体来讲是 json 库中定义的一个类型.它实现了 Marshaler 接口以及 Unmarshaler 接口,以此来支持序列化的能力.注意上面我们引用 官方 doc 的说明. ...
- ZCMU-1051
比较来说不太难其实,当然找到一定的公式这与前面的1033相识,都会用到f(i,j)=f(i-1,j)+f(i-1,j-1) 我们可以先从小部分看出来,一层可以整体或者两部分,在面对第i层看前面i-1层 ...
- WPF下,控件未响应鼠标属性触发器
WPF下,控件未响应鼠标属性触发器 记一次自定义控件调试 问题现象 问题分析 解决方式 记一次自定义控件调试 使用WPF写了个自定义控件,其中有个Button按钮,重写了样式模板 <Button ...
- 【转载】用shell命令一步步获取Java版本号
https://blog.csdn.net/f20052604/article/details/100269768 1.打印java -version命令echo $(java -version)输出 ...
- Qt音视频开发10-ffmpeg内核硬解码
一.前言 为了极大的降低CPU的占用,实现硬解码(也叫硬件加速)非常有必要,一个视频文件或者一路视频流还好,如果增加到64路视频流呢,如果是4K.8K这种高分辨率的视频呢,必须安装上硬解码才是上上策. ...
- Spring基础 01 | Ioc
Maven项目的创建 项目所在路径 - 项目一 - 创建Module - 添加Webapp(Project Structure) - 项目二 Spring简介 分层全栈(各层解决方案)轻量级框架,以I ...