第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数组 中,并返回 接收的字节数 。返回值分两种情况:

  1. 返回值小于传入的 count 参数: 读取位置已达到流的末尾,或流本身是以小块方式提供数据(通常是网络流)
  2. 返回值等于传入的 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​ ​才能进行查找。若流可查找,则:

  1. 流的 长度(Length) 可修改

    通过 SetLength​ 方法设置

    SetLength​ 的使用场景如下:

    1. 文件截断或扩展:当使用文件流(FileStream​)时,SetLength​ 方法可以用来截断或扩展文件。如果指定的长度小于当前文件大小,文件将被截断,超出的数据会被丢弃。如果指定的长度大于当前文件大小,文件将被扩展,新增的部分通常会用零字节填充。
    2. 调整内存流的大小:对于 MemoryStream​,SetLength​ 方法可以用来调整内存中存储的数据量。这可以在你需要更大或更小的缓冲区时非常有用。
  2. Position ​​ 属性可修改

    可以改变读写位置

  3. 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​ 静态字段,用于表示“空”流,常用于单元测试。

image

15.2.7 FileStream​ 类

15.2.7.1 创建 FileStream

实例化 FileStream ​有两种方式:

  1. 通过 File 类型的静态方法
  2. 通过 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
image

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 ​​ 标记枚举,其中包括:

    1. Encrypted​:请求操作系统加密。
    2. DeleteOnClose​:在文件关闭时自动删除临时文件。
    3. RandomAccess ​和 SequentialScan​:优化提示。
    4. 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 ​中的数据方式有二:

  1. ToArray ​ ​方法

    返回数据对应的 byte​ 数组。

  2. GetBuffer

    返回底层存储数组的引用,比流的实际长度要长。

Tips

MemoryStream​ 的关闭(Close​)和刷新(Flush​)不是必须的。MemoryStream​ 关闭后将无法再次读写,但是我们仍然可以调用 ToArray​ 方法来获得底层的数据。而刷新操作则不会对内存流执行任何操作。

15.2.9 PipeStream

管道类型有两种:

  1. ** 匿名 管道(速度更快):支持同一台计算机中的父进程和子进程之间进行 向**通信。
  2. ** 命名 管道(更加灵活):支持同一台不同计算机(使用 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 匿名管道

匿名管道也分为客户端、服务端,它具有如下特点:

  1. 仅支持 向通讯, 向通讯需 定义两个管道

    实例化时接受 PipeDirection ​枚举的 In ​或 Out ​作为参数确定通讯方向,不支持 InOut​。

  2. 通过 GetClientHandleAsString ​ 方法获取句柄信息,通过该信息客户端进行连接。

  3. 仅支持 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

image

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​ 仅支持处理 字节 ,一些类提供了高级的处理方式,具体如下:

image

  • 文本适配器

    • 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 ​能够读写:

  1. 基本的数据类型
  2. string
  3. 基础数据类型的数组

如下代码演示了二进制数据的读写:

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

    1. 会在文件开头和结尾处写入额外的协议信息,其中包括检测错误的 CRC。
    2. 遵循公认标准 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 ​进行操作

该类操作时与压缩软件别无二致,可以做到:

  1. 压缩

    ZipFile.CreateFromDirectory (@"d:\MyFolder,@"d:\compressed.zip");
    
  2. 解压

    ZipFile.ExtractToDirectory (@"d:\compressed.zip", @"d:\MyFolder");
    
  3. 速度优先还是体积优先等。

  4. 是否包含源目录名称

  5. 读写文件并遍历

    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);
    
  6. 向压缩包中添加文件,

    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);
    
  7. 删除压缩包文件

    ZipArchiveEntry.Delete ​方法

  8. 加压指定文件

    ZipFileExtensions.ExtractToFile ​方法

若使用 MemoryStream ​创建 ZipArchive​,可以完全在内存中进行操作。

15.6 文件与目录操作

System.IO​ 对文件、目录操作的接口有:

  • 静态类:File ​和 Directory
  • 实例方法:FileInfo ​和 DirectoryInfo

静态类 Path​:用于处理文件名称或者目录路径字符串。同时 Path 还可以用于临时文件的处理。

15.6.1 File 类

15.6.1.1 压缩与加密属性

本节讲解文件属性中的“压缩”和“加密”选项:

image

15.6.1.2 文件安全性

本节讲解权限控制信息:

image

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);    //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 内存映射文件

内存映射文件提供了两个主要特性:

  1. 高效地随机访问文件中的数据
  2. 在同一台计算机的不同进程间共享内存

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 内存映射文件和共享内存

内存映射文件可以被视为“内存中的文件”,不同进程可以访问同一“文件”,方式如下:

  1. 一个进程调用 MemoryMappedFile.CreateNew ​​ 创建共享内存。
  2. 另一个进程调用 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 应用程序就属于这种情况。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/862366.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

超全性能调优标准制定指南,你一定不能错过!

0 前言 我有个朋友说他们国企的系统从未性能调优,功能测试完就上线,线上也没性能问题,何必还做性能调优? 本文搞清:为什么要做性能调优? 啥时开始做? 做性能调优是不是有标准?1 为啥做性能调优?有些性能问题是慢慢产生,到了时间就自爆 更多性能问题是由访问量波动导致…

第16章 网络

第16章 网络 纲要 .NET Framework 在 System.Net.*命名空间中包含了支持各种网络标准的类,支持的标准包括 HTTP、TCP/IP 以及 FTP 等。以下列出了其中的主要组件:​Webclient​ 类 支持通过 HTTP 或者 FTP 执行简单的下载/上传操作。​WebRequest​ 和 WebResponse​ 类 可以…

第13章 诊断

第13章 诊断 13.1 条件编译 预编译的指令见 4.16 预处理指令,我们这里的条件编译用到的指令有:​#if​​、#else​​、#endif​​、#elif​​ 条件编译指令可以进行 与 ​&&​ ​、 或 ​||​ ​、 非 ​!​ 运算。预定义指令可以通过三种方式定义:在文件中通…

第14章 并发与异步

第14章 并发与异步 14.2 线程 进 程提供了程序执行的独立环境, 进 程持有 线 程,且至少持有一个 线 程。这些 线 程共享 进 程提供的执行环境。 14.2.1 创建线程 创建线程的步骤为:实例化 ​Thread​ ​ 对象,通过构造函数传入 ​ThreadStart​ ​ 委托。 调用 ​Thread…

Sqlserver With as 实现循环递归

一、脚本示例declare @Separator varchar(10), @str varchar(100) declare @l int, @i int select @Separator=,,@str=111,22,777,99,666 select @i = len(@Separator), @l = len(@str); with cte7 as ( select 0 a, 1 b union all select b, charindex(@Separator, @str, b)+@…

JAVA 分布式锁

分布式锁 JVM 自带的 synchronized 及 ReentrantLock 锁都是单进程内的,不能跨进程,如下,同时来个两个请求被分配到不同的tomcat,这种锁将失效:REDIS 实现分布式锁 可以借助 REDIS 的setnx 命令实现: https://blog.csdn.net/T_Y_F_/article/details/144238022 注:redis …

java8--类Scanner--文件内容输入--windows路径分隔符转义

try { Scanner in = new Scanner(Paths.get("C:\Users\Administrator\IdeaProjects\untitled2\src\test\myfile.txt"),"UTF-8"); } catch (IOException ioException) { ioException.printStackTrace(); }ps: 1.打印当前工…

[Windows] 启动 Windows Update 服务失败,报:Windows 无法启动 Windows Update 服务(位于 本地计算机 上) 错误 126:找不到指定的模块

1 问题描述现象1:Windows 10 家庭版-服务(services.msc)-启动 Windows Update 服务失败,报:"Windows 无法启动 Windows Update 服务(位于 本地计算机 上) 错误 126:找不到指定的模块"注: C:\Windows\System32\wuaueng.dll 文件存在注:注册表regedit:计算机\HKEY_L…

共享ubuntu系统宿主机的部分文件到win虚拟机--通过ISO文件挂载

安装genisoimage sudo apt-get update sudo apt-get install genisoimage将需要共享的文件放入指定文件夹 cp /path/to/your/file ~/iso_work/使用genisoimage生成新镜像 genisoimage -o /path/to/new.iso -J -R -V "NEW_ISO_LABEL" ~/iso_work/其中new.iso就是新镜像…

Luogu P9646 SNCPC2019 Paper-cutting 题解 [ 紫 ] [ manacher ] [ 贪心 ] [ 哈希 ] [ BFS ]

manacher 与贪心的好题。Paper-cutting:思维很好,但代码很构式的 manacher 题。 蒟蒻 2025 年切的第一道题,是个紫,并且基本独立想出的,特此纪念。 判断能否折叠 我们先考虑一部分能折叠需要满足什么条件。显然,这一部分需要是一个长度为偶数的回文串。 那么横向和纵向会…

深度学习基础理论————分布式训练(模型并行/数据并行/流水线并行/张量并行)

主要介绍Pytorch分布式训练代码以及原理以及一些简易的Demo代码 模型并行 是指将一个模型的不同部分(如层或子模块)分配到不同的设备上运行。它通常用于非常大的模型,这些模型无法完整地放入单个设备的内存中。在模型并行中,数据会顺序通过各个层,即一层处理完所有数据之后…

overleaf-Latex教程

1.领取免费服务器,推荐免费服务器(SanFengYun)见下图。2.安装宝塔面板,配置内网为127.0.0.1,访问外网地址。 3.可以在宝塔面板一键部署网站,输入自己的域名即可。 4.关键:安装docker,安装yum,设置github可以访问。 5.更换docker镜像,自带镜像无法访问 6.按照overleaf…