计算机系统应用教程网站

网站首页 > 技术文章 正文

C# 13 和 .NET 9 全知道 :9 处理文件、流和序列化 (1)

btikc 2025-01-16 18:10:32 技术文章 18 ℃ 0 评论

本章讨论文件和流的读写、文本编码和序列化。不与文件系统交互的应用程序极为罕见。作为一名 .NET 开发人员,几乎每个您构建的应用程序都需要管理文件系统,并创建、打开、读取和写入文件。这些文件大多数将包含文本,因此理解文本是如何编码的非常重要。最后,在内存中处理对象后,您需要将它们永久存储以便后续重用。您可以使用一种称为序列化的技术来实现这一点。

在本章中,我们将涵盖以下主题:

  • 管理文件系统
  • 使用流进行读写
  • 编码和解码文本
  • 序列化对象图

管理文件系统

您的应用程序通常需要在不同环境中执行与文件和目录的输入和输出操作。 SystemSystem.IO 命名空间包含用于此目的的类。

处理跨平台环境和文件系统

让我们探讨如何处理跨平台环境以及 Windows、Linux 和 macOS 之间的差异。Windows、macOS 和 Linux 的路径是不同的,因此我们将首先探讨.NET 是如何处理这个问题的:

  1. 使用您首选的代码编辑器创建一个新项目,如下列表所定义:项目模板:控制台应用程序 / console项目文件和文件夹: WorkingWithFileSystems解决方案文件和文件夹: Chapter09
  2. 在项目文件中,为 Spectre.Console 添加一个包引用,然后添加元素以静态和全局方式导入以下类 System.ConsoleSystem.IO.DirectorySystem.IO.PathSystem.Environment ,如以下标记所示:
<ItemGroup>
  <PackageReference Include="Spectre.Console" Version="0.47.0" />
</ItemGroup>
<ItemGroup>
  <Using Include="System.Console" Static="true" />
  <Using Include="System.IO.Directory" Static="true" />
  <Using Include="System.IO.Path" Static="true" />
  <Using Include="System.Environment" Static="true" />
</ItemGroup>
  1. 构建 WorkingWithFileSystems 项目以恢复包。
  2. 添加一个名为 Program.Helpers.cs 的新类文件。
  3. Program.Helpers.cs 中,添加一个部分 Program 类,包含一个 SectionTitle 方法,如下代码所示:
// null namespace to merge with auto-generated Program.
partial class Program
{
  private static void SectionTitle(string title)
  {
    WriteLine();
    ConsoleColor previousColor = ForegroundColor;
    // Use a color that stands out on your system.
    ForegroundColor = ConsoleColor.DarkYellow;
    WriteLine(#34;*** {title} ***");
    ForegroundColor = previousColor;
  }
}

Program.cs 中,添加语句以使用 Spectre.Console 表执行以下操作:

  • 输出路径和目录分隔符。
  • 输出当前目录的路径。
  • 输出一些系统文件、临时文件和文档的特殊路径:
using Spectre.Console; // To use Table.
#region Handling cross-platform environments and filesystems
SectionTitle("Handling cross-platform environments and filesystems");
// Create a Spectre Console table.
Table table = new();
// Add two columns with markup for colors.
table.AddColumn("[blue]MEMBER[/]");
table.AddColumn("[blue]VALUE[/]");
// Add rows.
table.AddRow("Path.PathSeparator", PathSeparator.ToString());
table.AddRow("Path.DirectorySeparatorChar",
  DirectorySeparatorChar.ToString());
table.AddRow("Directory.GetCurrentDirectory()",
  GetCurrentDirectory());
table.AddRow("Environment.CurrentDirectory", CurrentDirectory);
table.AddRow("Environment.SystemDirectory", SystemDirectory);
table.AddRow("Path.GetTempPath()", GetTempPath());
table.AddRow("");
table.AddRow("GetFolderPath(SpecialFolder", "");
table.AddRow("  .System)", GetFolderPath(SpecialFolder.System));
table.AddRow("  .ApplicationData)",
  GetFolderPath(SpecialFolder.ApplicationData));
table.AddRow("  .MyDocuments)",
  GetFolderPath(SpecialFolder.MyDocuments));
table.AddRow("  .Personal)",
  GetFolderPath(SpecialFolder.Personal));
// Render the table to the console
AnsiConsole.Write(table);
#endregion
  1. Environment 类型还有许多其他有用的成员,我们在这段代码中没有使用,包括 OSVersionProcessorCount 属性。
  1. 运行代码并查看结果,如图 9.1 所示,使用 Windows 上的 Visual Studio。

更多信息:您可以通过以下链接了解有关使用 Spectre Console 表的更多信息:https://spectreconsole.net/widgets/table。

在 Mac 上使用 dotnet run 运行控制台应用程序时,路径和目录分隔符字符是不同的, CurrentDirectory 将是项目文件夹,而不是 bin 中的一个文件夹,如图 9.2 所示:

良好实践:Windows 使用反斜杠 ( \ ) 作为目录分隔符。macOS 和 Linux 使用斜杠 ( / ) 作为目录分隔符。在组合路径时,不要假设代码中使用的是哪个字符;请使用 Path.DirectorySeparatorChar

在本章的后续部分,我们将在 Personal 特殊文件夹中创建目录和文件,因此请记下该文件夹在您的操作系统中的位置。例如,如果您使用的是 Linux,它应该是 $USER/Documents

管理驱动器

要管理驱动器,请使用 DriveInfo 类型,该类型具有一个静态方法,可以返回有关连接到您计算机的所有驱动器的信息。每个驱动器都有一个驱动器类型。

让我们来探索驱动器:

  1. Program.cs 中,编写语句以获取所有驱动器并输出它们的名称、类型、大小、可用空闲空间和格式,但仅在驱动器准备就绪时,如以下代码所示:
SectionTitle("Managing drives");
Table drives = new();
drives.AddColumn("[blue]NAME[/]");
drives.AddColumn("[blue]TYPE[/]");
drives.AddColumn("[blue]FORMAT[/]");
drives.AddColumn(new TableColumn(
  "[blue]SIZE (BYTES)[/]").RightAligned());
drives.AddColumn(new TableColumn(
  "[blue]FREE SPACE[/]").RightAligned());
foreach (DriveInfo drive in DriveInfo.GetDrives())
{
  if (drive.IsReady)
  {
    drives.AddRow(drive.Name, drive.DriveType.ToString(),
      drive.DriveFormat, drive.TotalSize.ToString("N0"),
      drive.AvailableFreeSpace.ToString("N0"));
  }
  else
  {
    drives.AddRow(drive.Name, drive.DriveType.ToString(),
      string.Empty, string.Empty, string.Empty);
  }
}
AnsiConsole.Write(drives);

良好实践:在读取属性如 TotalSize 之前检查驱动器是否准备就绪,否则您将看到可移动驱动器抛出的异常。

在 Linux 上,默认情况下,当以普通用户身份运行时,您的控制台应用程序仅有权限读取 NameDriveType 属性。对于 DriveFormatTotalSizeAvailableFreeSpace 会抛出 UnauthorizedAccessException 。以超级用户身份运行控制台应用程序以避免此问题,如以下命令所示: sudo dotnet run 。在开发环境中使用 sudo 是可以的,但在生产环境中,建议编辑您的权限以避免以提升的权限运行。在 Linux 上,名称和驱动格式列可能也需要更宽,例如,分别为 55 和 12 个字符宽。

  1. 运行代码并查看结果,如图 9.3 所示:

管理目录

要管理目录,请使用 DirectoryPathEnvironment 静态类。这些类型包含许多与文件系统交互的成员。

在构建自定义路径时,您必须小心编写代码,以确保它不对平台做出任何假设,例如,使用什么作为目录分隔符字符:

  1. Program.cs 中,编写语句以执行以下操作:在用户的主目录下定义一个自定义路径,通过为目录名称创建一个字符串数组,然后使用 Path 类型的 Combine 方法正确地将它们组合在一起。使用 Directory 类的 Exists 方法检查自定义目录路径的存在性。使用 Directory 类的 CreateDirectoryDelete 方法创建并删除目录,包括其中的文件和子目录:
SectionTitle("Managing directories");
string newFolder = Combine(
  GetFolderPath(SpecialFolder.Personal), "NewFolder");
WriteLine(#34;Working with: {newFolder}");
// We must explicitly say which Exists method to use
// because we statically imported both Path and Directory.
WriteLine(#34;Does it exist? {Path.Exists(newFolder)}");
WriteLine("Creating it...");
CreateDirectory(newFolder);
// Let's use the Directory.Exists method this time.
WriteLine(#34;Does it exist? {Directory.Exists(newFolder)}");
Write("Confirm the directory exists, and then press any key.");
ReadKey(intercept: true);
WriteLine("Deleting it...");
Delete(newFolder, recursive: true);
WriteLine(#34;Does it exist? {Path.Exists(newFolder)}");
  1. 在 .NET 6 及更早版本中,只有 Directory 类具有 Exists 方法。在 .NET 7 或更高版本中, Path 类也具有 Exists 方法。两者都可以用于检查路径的存在性。
  1. 运行代码,查看结果,并使用您喜欢的文件管理工具确认目录已被创建,然后再按 Enter 删除它,如下所示的输出:
Working with: C:\Users\markj\OneDrive\Documents\NewFolder
Does it exist? False
Creating it...
Does it exist? True
Confirm the directory exists, and then press any key.
Deleting it...
Does it exist? False

管理文件

在处理文件时,您可以静态导入文件类型,就像我们对目录类型所做的那样。然而,在下一个示例中,我们将不这样做,因为它与目录类型有一些相同的方法,这会导致冲突。文件类型的名称足够简短,在这种情况下并不重要。步骤如下:

  1. Program.cs 中,编写语句以执行以下操作:检查文件是否存在。创建一个文本文件。将一行文本写入文件。关闭文件以释放系统资源和文件锁(这通常在 try - finally 语句块中完成,以确保文件被关闭,即使在写入时发生异常)。将文件复制到备份。删除原始文件。读取备份文件的内容,然后关闭它:
SectionTitle("Managing files");
// Define a directory path to output files starting
// in the user's folder.
string dir = Combine(
  GetFolderPath(SpecialFolder.Personal), "OutputFiles");
CreateDirectory(dir);
// Define file paths.
string textFile = Combine(dir, "Dummy.txt");
string backupFile = Combine(dir, "Dummy.bak");
WriteLine(#34;Working with: {textFile}");
WriteLine(#34;Does it exist? {File.Exists(textFile)}");
// Create a new text file and write a line to it.
StreamWriter textWriter = File.CreateText(textFile);
textWriter.WriteLine("Hello, C#!");
textWriter.Close(); // Close file and release resources.
WriteLine(#34;Does it exist? {File.Exists(textFile)}");
// Copy the file, and overwrite if it already exists.
File.Copy(sourceFileName: textFile,
  destFileName: backupFile, overwrite: true);
WriteLine(
  #34;Does {backupFile} exist? {File.Exists(backupFile)}");
Write("Confirm the files exist, and then press any key.");
ReadKey(intercept: true);
// Delete the file.
File.Delete(textFile);
WriteLine(#34;Does it exist? {File.Exists(textFile)}");
// Read from the text file backup.
WriteLine(#34;Reading contents of {backupFile}:");
StreamReader textReader = File.OpenText(backupFile);
WriteLine(textReader.ReadToEnd());
textReader.Close();

运行代码并查看结果,如下所示的输出:

Working with: C:\Users\markj\OneDrive\Documents\OutputFiles\Dummy.txt
Does it exist? False
Does it exist? True
Does C:\Users\markj\OneDrive\Documents\OutputFiles\Dummy.bak exist? True
Confirm the files exist, and then press any key.
Does it exist? False
Reading contents of C:\Users\markj\OneDrive\Documents\OutputFiles\Dummy.bak:
Hello, C#!

管理路径

有时,您需要处理路径的某些部分;例如,您可能只想提取文件夹名称、文件名或扩展名。有时,您需要生成临时文件夹和文件名。您可以使用 Path 类的静态方法来实现这一点:

  1. Program.cs 中,添加以下语句:
SectionTitle("Managing paths");
WriteLine(#34;Folder Name: {GetDirectoryName(textFile)}");
WriteLine(#34;File Name: {GetFileName(textFile)}");
WriteLine("File Name without Extension: {0}",
  GetFileNameWithoutExtension(textFile));
WriteLine(#34;File Extension: {GetExtension(textFile)}");
WriteLine(#34;Random File Name: {GetRandomFileName()}");
WriteLine(#34;Temporary File Name: {GetTempFileName()}");

运行代码并查看结果,如下所示的输出:

Folder Name: C:\Users\markj\OneDrive\Documents\OutputFiles
File Name: Dummy.txt
File Name without Extension: Dummy
File Extension: .txt
Random File Name: u45w1zki.co3
Temporary File Name:
C:\Users\markj\AppData\Local\Temp\tmphdmipz.tmp

etTempFileName 创建一个零字节文件并返回其名称,准备供您使用。 GetRandomFileName 仅返回一个文件名;它并不创建文件。

获取文件信息

要获取有关文件或目录的更多信息,例如其大小或最后访问时间,您可以创建 FileInfoDirectoryInfo 类的实例。

FileInfoDirectoryInfo 都继承自 FileSystemInfo ,因此它们都有像 LastAccessTimeDelete 这样的成员,以及特定于它们自己的额外成员,如表 9.1 所示:

成员

FileSystemInfo

字段: FullPathOriginalPath

属性: AttributesCreationTimeCreationTimeUtcExistsExtensionFullNameLastAccessTimeLastAccessTimeUtcLastWriteTimeLastWriteTimeUtc ,和 Name

方法: DeleteGetObjectData ,和 Refresh

DirectoryInfo

属性: ParentRoot

方法: CreateCreateSubdirectoryEnumerateDirectoriesEnumerateFilesEnumerateFileSystemInfosGetAccessControlGetDirectoriesGetFilesGetFileSystemInfosMoveTo ,和 SetAccessControl

FileInfo

属性: DirectoryDirectoryNameIsReadOnly ,和 Length

方法: AppendTextCopyToCreateCreateTextDecryptEncryptGetAccessControlMoveToOpenOpenReadOpenTextOpenWriteReplace ,和 SetAccessControl

表 9.1:获取文件和目录信息的类

让我们编写一些代码,使用一个 FileInfo 实例高效地对文件执行多个操作:

  1. Program.cs 中,添加语句以创建备份文件的 FileInfo 实例,并将其信息写入控制台,如以下代码所示:
SectionTitle("Getting file information");
FileInfo info = new(backupFile);
WriteLine(#34;{backupFile}:");
WriteLine(#34;  Contains {info.Length} bytes.");
WriteLine(#34;  Last accessed: {info.LastAccessTime}");
WriteLine(#34;  Has readonly set to {info.IsReadOnly}.");

运行代码并查看结果,如下所示的输出:

C:\Users\markj\OneDrive\Documents\OutputFiles\Dummy.bak:
  Contains 12 bytes.
  Last accessed: 13/07/2023 12:11:12
  Has readonly set to False.

字节数在您的操作系统上可能会有所不同,因为操作系统可以使用不同的行结束符。

控制您与文件的工作方式

在处理文件时,您通常需要控制它们的打开方式。 File.Open 方法有重载以使用 enum 值指定额外选项。

enum 类型如下:

  • FileMode : 这控制您想对文件执行的操作,例如 CreateNewOpenOrCreateTruncate
  • FileAccess : 这控制您需要的访问级别,例如 ReadWrite
  • FileShare : 这控制文件上的锁,以允许其他进程获得指定级别的访问权限,例如 Read

您可能想要打开一个文件并从中读取,同时允许其他进程也读取,如以下代码所示:

FileStream file = File.Open(pathToFile,
  FileMode.Open, FileAccess.Read, FileShare.Read);

还有一个 enum 用于文件的属性 FileAttributes ,它检查 FileSystemInfo 派生类型的 Attributes 属性的值,如 ArchiveEncrypted 。例如,您可以检查文件或目录的属性,如以下代码所示:

FileInfo info = new(backupFile);
WriteLine("Is the backup file compressed? {0}",
  info.Attributes.HasFlag(FileAttributes.Compressed));

File 类的所有管理文件的方法都有一个参数,用于指定文件的路径,作为 string 值。 File 类的其他参数、其方法及方法返回的内容见表 9.2:

方法

特殊参数

返回

笔记

Open

FileMode, FileAccess, FileShare

FileStream

对文件的字节级访问。

OpenWrite


FileStream

从开始覆盖但不截断。

Create

FileOptions

FileStream

覆盖和截断。

OpenText


StreamReader

用于读取文本文件。

CreateText


StreamWriter

覆盖和截断。

AppendText


StreamWriter

如果文件不存在,则创建该文件。

ReadAllLines

Encoding

string[]

警告!这对于大文件使用了大量内存。

ReadAllText

Encoding

string

警告!这对于大文件使用了大量内存。

WriteAllText

string, Encoding

void


AppendAllText

string, Encoding

void


WriteAllLines

string[], IEnumerable<string>, Encoding

void


AppendAllLines

string[], IEnumerable<string>, Encoding

void


表 9.2:文件类及其方法

现在您已经学习了一些在文件系统中处理目录和文件的常见方法,接下来我们需要学习如何读取和写入存储在文件中的数据,也就是如何处理流。

使用流进行读写

在第 10 章《使用 Entity Framework Core 处理数据》中,您将使用一个名为 Northwind.db 的文件,但您不会直接处理该文件。相反,您将与 SQLite 数据库引擎进行交互,后者将读取和写入该文件。在没有其他系统“拥有”该文件并为您进行读写的情况下,您将使用文件流直接处理该文件。

流是一系列可以读取和写入的字节。尽管文件可以像数组一样处理,通过知道文件中某个字节的位置提供随机访问,但以流的方式处理文件更为高效,在这种方式中,字节可以按顺序访问。当人类进行处理时,他们往往需要随机访问,以便能够在数据中跳转、进行更改,然后返回到之前处理过的数据。当自动化系统进行处理时,它往往能够顺序工作,只需“接触”数据一次。

流也可以用于处理终端输入和输出以及网络资源,例如不提供随机访问且无法定位(即移动)到某个位置的套接字和端口。您可以编写代码来处理一些任意字节,而无需知道或关心它们来自哪里。您的代码只是简单地读取或写入流,另一段代码处理字节存储的位置。

理解抽象流和具体流

有一个名为 Streamabstract 类,表示任何类型的流。请记住, abstract 类不能使用 new 实例化;它只能被继承。这是因为它只实现了一部分。

有许多具体类继承自这个基类,包括 FileStreamMemoryStreamBufferedStreamGZipStreamSslStream 。它们的工作方式相同。所有流都实现了 IDisposable ,因此它们都有一个 Dispose 方法来释放非托管资源。

Stream 类的一些常见成员在表 9.3 中描述:

成员

描述

CanReadCanWrite

这些属性决定了您是否可以从流中读取和写入。

LengthPosition

这些属性决定了字节的总数和当前在流中的位置。这些属性可能会对某些类型的流抛出 NotSupportedException ,例如,如果 CanSeek 返回 false

CloseDispose

此方法关闭流并释放其资源。您可以调用任一方法,因为 Dispose 的实现调用了 Close

Flush

如果流有一个缓冲区,则此方法将缓冲区中的字节写入流,并清空缓冲区。

CanSeek

此属性确定是否可以使用 Seek 方法。

Seek

此方法将当前位置移动到其参数指定的位置。

ReadReadAsync

这些方法从流中读取指定数量的字节到字节数组中,并移动位置。

ReadByte

此方法从流中读取下一个字节并推进位置。

WriteWriteAsync

这些方法将字节数组的内容写入流中。

WriteByte

此方法将一个字节写入流中。

表 9.3:Stream 类的常见成员

理解存储流

一些表示字节将被存储位置的存储流在表 9.4 中进行了描述:

命名空间

描述

System.IO

FileStream

文件系统中存储的字节

System.IO

MemoryStream

当前进程中存储在内存中的字节

System.Net.Sockets

NetworkStream

存储在网络位置的字节

表 9.4:存储流类

FileStream 已在 .NET 6 中重写,以在 Windows 上实现更高的性能和可靠性。您可以通过以下链接了解更多信息:https://devblogs.microsoft.com/dotnet/file-io-improvements-in-dotnet-6/.

理解函数流

功能流不能独立存在,只能“插入”其他流以添加功能。某些功能在表 9.5 中描述:

命名空间

描述

System.Security.Cryptography

CryptoStream

这加密和解密流。

System.IO.Compression

GZipStream, DeflateStream

这些压缩和解压缩流。

System.Net.Security

AuthenticatedStream

这在流中发送凭据。

表 9.5:功能流类

理解流助手

尽管有时您需要在低级别处理流,但通常情况下,您可以将辅助类插入链中以简化操作。所有流的辅助类型都实现了 IDisposable ,因此它们具有 Dispose 方法来释放非托管资源。

处理常见场景的一些辅助类在表 9.6 中描述:

命名空间

描述

System.IO

StreamReader

这从底层流中读取为纯文本。

System.IO

StreamWriter

这将以纯文本形式写入底层流。

System.IO

BinaryReader

这从流中读取 .NET 类型。例如, ReadDecimal 方法从基础流中读取下一个 16 字节作为 decimal 值,而 ReadInt32 方法读取下一个 4 字节作为 int 值。

System.IO

BinaryWriter

这将以 .NET 类型写入流。例如, Write 方法使用 decimal 参数向底层流写入 16 字节,而 Write 方法使用 int 参数写入 4 字节。

System.Xml

XmlReader

这使用 XML 格式从底层流中读取。

System.Xml

XmlWriter

这使用 XML 格式写入底层流。

表 9.6:流辅助类

构建流管道

将一个辅助工具,如 StreamWriter ,和多个函数流,如 GZipStreamCryptoStream ,与一个存储流,如 FileStream ,组合成一个管道是非常常见的,如图 9.4 所示:

您的代码只需调用一个简单的辅助方法,例如 WriteLine ,以通过管道发送一个 string 值,例如 "Hello" ,直到它到达最终目的地,经过压缩和加密后,以 " G7x" (或其他形式)写入文件。

良好实践:“良好的加密将生成相对不可压缩的数据。如果您更改操作的顺序,先进行压缩再进行加密,不仅最终文件会更小,而且加密所需的时间也可能更少,因为它将处理更少的数据。”摘自 Stephen Toub 的文章:https://learn.microsoft.com/en-us/archive/msdn-magazine/2008/february/net-matters-stream-pipeline。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表