XIKEW.COM - 实用教程 - C# 8.0&.NET Core 3.0流的读写 - 实用教程,C# 8.0, .NET Core 3.0,Modern Cross-Platform Development, Chapter 09, Working with Files, Streams, and Serialization - 关于《C# 8.0 And .NET Core 3.0 Modern Cross-Platform Development》第九章流处理,重点介绍了兼容性最佳的XML格式及体积最小的JSON格式

C# 8.0&.NET Core 3.0流的读写
NETCORE 5/24/2020 1:25:36 PM 阅读:4

关于《C# 8.0 And .NET Core 3.0 Modern Cross-Platform Development》第九章流处理,重点介绍了兼容性最佳的XML格式及体积最小的JSON格式

关键字:C# 8.0, .NET Core 3.0,Modern Cross-Platform Development, Chapter 09, Working with Files, Streams, and Serialization

使用流进行读写

流是一个可以读写的字节序列。虽然文件可以像数组一样进行处理,但是通过了解字节在文件中的位置提供随机访问,可以将文件作为一个流进行处理,其中字节可以按顺序访问,这很有用。

流还可以用于处理终端的输入和输出以及网络资源,例如不提供随机访问且无法定位的套接字和端口。 您可以编写代码来处理一些任意字节,而无需知道或关心它的来源。 您的代码仅读取或写入一个流,另一段代码处理实际存储字节的位置。

有一个名为Stream的抽象类,它代表一个流。 有很多从该基类继承的类,包括FileStream,MemoryStream,BufferedStream,GZipStream和SslStream,因此它们都以相同的方式工作。

所有Stream都继承IDisposable,因此它们具有Dispose方法来释放非托管资源。

下表是Stream类的一些常见成员:

| 方法成员 | 描述说明 | | --- | --- | |CanRead , CanWrite|这决定了您是否可以读取和写入流。| |Length , Position|这确定了字节总数和流中的当前位置。 这些属性可能会为某些类型的流抛出异常。| |Dispose()|这将关闭流并释放其资源。| |Flush()|如果流中有缓冲区,则将缓冲区中的字节写入流中并清除缓冲区。| |Read() , ReadAsync()|这将从流中读取指定数量的字节到字节数组中推进读取位置。| |ReadByte()|它从流中读取下一个字节并推进读取位置。| |Seek()|这会将读取位置移动到指定位置(如果CanSeek属性为true)。| |Write() , WriteAsync()|这会将字节数组的内容写入流中。| |WriteByte()|这会将一个字节写入流。|

下表中是一些存储流,表示存储字节的位置:

|空间名|类名|描述说明| | --- | --- | --- | |System.IO|FileStream|字节储存在文件系统中| |System.IO|MemoryStream|字节储存在当前进程的内存中| |System.Net.Sockets|NetworkStream|字节储存在网络地址中|

下表列出了一些不能单独存在的功能流。 它们只能“插入”其他流以添加功能:

|空间名|类名|描述说明| | --- | --- | --- | |System.Security .Cryptography|CryptoStream|对流进行加密和解密。| |System.IO.Compression|GZipStream , DeflateStream|对流压缩和解压缩。| |System.Net.Security|AuthenticatedStream|对跨流发送凭据。|

尽管有时您需要以较低的级别使用流,但大多数情况下,您可以将帮助程序类插入链中以使事情变得更容易。

流的所有帮助程序类型都实现IDisposable,因此它们具有Dispose方法来释放非托管资源。

下表是一些用于处理常见方案的帮助程序类:

|空间名|类名|描述说明| | --- | --- | --- | |System.IO|StreamReader|从底层流中读取文本。| |System.IO|StreamWriter|以文本形式写入底层流。| |System.IO|BinaryReader|从流中读取.NET类型。例如ReadDecimal方法以Decimal的形式从底层流中读取后面的16个字节,ReadInt32方法以int值的形式读取后面的4个字节。|| |System.IO|BinaryWriter|这将以.NET类型的形式写入流。例如,带有Decimal参数的Write方法向底层流写入16个字节,而带有Int参数的Write方法写入4个字节。| |System.Xml|XmlReader|从底层流中读取XML。| |System.Xml|XmlWriter|以XML的形式写入底层流。|

写入文本流

让我们输入一些代码来将文本写入流。

  1. 创建一个名为WorkingWithStreams的新控制台应用程序项目,将其添加到Chapter09工作区,然后选择该项目作为OmniSharp的活动项目。
  2. 导入System.IO和System.Xml命名空间,并静态导入System.Console,System.Environment和System.IO.Path类型。
  3. 定义一个可能包含“蝰蛇飞行员”呼号的字符串值数组,并创建一个枚举呼号的WorkWithText方法,将每个单独的行写在一个文本文件中,如以下代码所示:
// define an array of Viper pilot call signs
static string[] callsigns = new string[] { "Husker", "Starbuck", "Apollo", "Boomer","Bulldog", "Athena", "Helo", "Racetrack" };
static void WorkWithText()
{
    // define a file to write to
    string textFile = Combine(CurrentDirectory, "streams.txt");
    // create a text file and return a helper writer
    StreamWriter text = File.CreateText(textFile);
    // enumerate the strings, writing each one
    // to the stream on a separate line
    foreach (string item in callsigns)
    {
        text.WriteLine(item);
    }
    text.Close(); // release resources
    
    // output the contents of the file
    WriteLine("{0} contains {1:N0} bytes.",
        arg0: textFile,
        arg1: new FileInfo(textFile).Length);
    WriteLine(File.ReadAllText(textFile));
}
  1. 在Main中,调用WorkWithText方法。
  2. 运行应用程序并查看结果,如下面的输出所示:
/Users/markjprice/Code/Chapter09/WorkingWithStreams/streams.txt contains 60 bytes.
Husker
Starbuck
Apollo
Boomer
Bulldog
Athena
Helo
Racetrack
  1. 打开创建的文件并检查它是否包含调用符号列表。

写入XML流

编写XML元素有两种方法,如下:

  • WriteStartElement和WriteEndElement:当一个元素可能有子元素时,使用此对。
  • WriteElementString:当元素没有子元素时使用此元素。

现在,让我们尝试在XML文件中存储相同的字符串值数组。

  1. 创建一个列举调用符号的WorkWithXml方法,将每个调用符号作为一个元素写入一个XML文件中,如下面的代码所示:
static void WorkWithXml()
{
    // define a file to write to
    string xmlFile = Combine(CurrentDirectory, "streams.xml");
    // create a file stream
    FileStream xmlFileStream = File.Create(xmlFile);
    // wrap the file stream in an XML writer helper
    // and automatically indent nested elements
    XmlWriter xml = XmlWriter.Create(xmlFileStream,
    new XmlWriterSettings { Indent = true });
    // write the XML declaration
    xml.WriteStartDocument();
    // write a root element
    xml.WriteStartElement("callsigns");
    // enumerate the strings writing each one to the stream
    foreach (string item in callsigns)
    {
        xml.WriteElementString("callsign", item);
    }
    // write the close root element
    xml.WriteEndElement();
    // close helper and stream
    xml.Close();
    xmlFileStream.Close();
    // output all the contents of the file
    WriteLine("{0} contains {1:N0} bytes.",
    arg0: xmlFile,
    arg1: new FileInfo(xmlFile).Length);
    WriteLine(File.ReadAllText(xmlFile));
}
  1. 在Main中,注释上一个方法调用,然后将调用添加到WorkWithXml方法。
  2. 运行应用程序并查看结果,如下面的输出所示:
/Users/markjprice/Code/Chapter09/WorkingWithStreams/streams.xml contains 310 bytes.
<?xml version="1.0" encoding="utf-8"?>
<callsigns>
<callsign>Husker</callsign>
<callsign>Starbuck</callsign>
<callsign>Apollo</callsign>
<callsign>Boomer</callsign>
<callsign>Bulldog</callsign>
<callsign>Athena</callsign>
<callsign>Helo</callsign>
<callsign>Racetrack</callsign>
</callsigns>

处置文件资源

当您打开文件进行读写时,您正在使用.NET以外的资源。 这些称为非托管资源,在使用完它们后必须将其丢弃。 为了保证它们被清除,我们可以在finally块内部调用Dispose方法。

  1. 让我们改进与XML一起使用的先前代码,以正确处理其非托管资源。
static void WorkWithXml()
{
    FileStream xmlFileStream = null;
    XmlWriter xml = null;
    try
    {
        // define a file to write to
        string xmlFile = Combine(CurrentDirectory, "streams.xml");
        // create a file stream
        xmlFileStream = File.Create(xmlFile);
        // wrap the file stream in an XML writer helper
        // and automatically indent nested elements
        xml = XmlWriter.Create(xmlFileStream,
        new XmlWriterSettings { Indent = true });
        // write the XML declaration
        xml.WriteStartDocument();
        // write a root element
        xml.WriteStartElement("callsigns");
        // enumerate the strings writing each one to the stream
        foreach (string item in callsigns)
        {
            xml.WriteElementString("callsign", item);
        }
        // write the close root element
        xml.WriteEndElement();
        // close helper and stream
        xml.Close();
        xmlFileStream.Close();
        // output all the contents of the file
        WriteLine($"{0} contains {1:N0} bytes.",
        arg0: xmlFile,
        arg1: new FileInfo(xmlFile).Length);
        WriteLine(File.ReadAllText(xmlFile));
    }
    catch (Exception ex)
    {
        // if the path doesn't exist the exception will be caught
        WriteLine($"{ex.GetType()} says {ex.Message}");
    }
    finally
    {
        if (xml != null)
        {
            xml.Dispose();
            WriteLine("The XML writer's unmanaged resources have been disposed.");
        }
        if (xmlFileStream != null)
        {
            xmlFileStream.Dispose();
            WriteLine("The file stream's unmanaged resources have been disposed.");
        }
    }
}

你也可以返回并修改你之前创建的其他方法,这里作为一个可选练习留给你。

  1. 运行应用程序并查看结果,如下面的输出所示:
The XML writer's unmanaged resources have been disposed.
The file stream's unmanaged resources have been disposed.

优化提示:在调用Dispose之前,请检查该对象是否为null。

您可以简化需要检查空对象的代码,然后使用using语句调用其Dispose方法。 using关键字有两种用法容易混淆:导入名称空间和生成对实现IDisposable的对象调用Dispose的finally语句。 编译器将using语句块更改为try-finally语句,而不包含catch语句。 您可以使用嵌套的try语句; 因此,如果您确实想捕获任何异常,则可以如以下代码示例所示:

using (FileStream file2 = File.OpenWrite(Path.Combine(path, "file2.txt")))
{
    using (StreamWriter writer2 = new StreamWriter(file2))
    {
        try
        {
            writer2.WriteLine("Welcome, .NET Core!");
        }
        catch (Exception ex)
        {
            WriteLine($"{ex.GetType()} says {ex.Message}");
        }
    } // automatically calls Dispose if the object is not null
} // automatically calls Dispose if the object is not null

压缩流

XML相对冗长,因此比纯文本占用更多的字节空间。 我们可以使用一种称为GZIP的通用压缩算法来压缩XML。

  1. 导入以下名称空间:
using System.IO.Compression;
  1. 添加一个WorkWithCompression方法,该方法使用GZipSteam的实例创建一个包含与以前相同的XML元素的压缩文件,然后在读取该文件并将其输出到控制台时对其进行解压缩,如以下代码所示:
static void WorkWithCompression()
{
    // compress the XML output
    string gzipFilePath = Combine(CurrentDirectory, "streams.gzip");
    FileStream gzipFile = File.Create(gzipFilePath);
    using (GZipStream compressor = new GZipStream(gzipFile, CompressionMode.Compress))
    {
        using (XmlWriter xmlGzip = XmlWriter.Create(compressor))
        {
            xmlGzip.WriteStartDocument();
            xmlGzip.WriteStartElement("callsigns");
            foreach (string item in callsigns)
            {
                xmlGzip.WriteElementString("callsign", item);
            }
            // the normal call to WriteEndElement is not necessary
            // because when the XmlWriter disposes, it will
            // automatically end any elements of any depth
        }
    } // also closes the underlying stream
      // output all the contents of the compressed file
    WriteLine("{0} contains {1:N0} bytes.",
    gzipFilePath, new FileInfo(gzipFilePath).Length);
    WriteLine($"The compressed contents:");
    WriteLine(File.ReadAllText(gzipFilePath));
    // read a compressed file
    WriteLine("Reading the compressed XML file:");
    gzipFile = File.Open(gzipFilePath, FileMode.Open);
    using (GZipStream decompressor = new GZipStream(gzipFile, CompressionMode.Decompress))
    {
        using (XmlReader reader = XmlReader.Create(decompressor))
        {
            while (reader.Read()) // read the next XML node
            {
                // check if we are on an element node named callsign
                if ((reader.NodeType == XmlNodeType.Element)
                && (reader.Name == "callsign"))
                {
                    reader.Read(); // move to the text inside element
                    WriteLine($"{reader.Value}"); // read its value
                }
            }
        }
    }
}
  1. 在Main中,保留对WorkWithXml的调用,并添加对WorkWithCompression的调用,如以下代码所示:
static void Main(string[] args)
{
    // WorkWithText();
    WorkWithXml();
    WorkWithCompression();
}
  1. 运行控制台应用程序,并比较XML文件和压缩XML文件的大小。 它小于未压缩的相同XML的大小的一半,如以下编辑的输出所示:
/Users/markjprice/Code/Chapter09/WorkingWithStreams/streams.xml contains 310 bytes.
/Users/markjprice/Code/Chapter09/WorkingWithStreams/streams.gzip contains 150 bytes.

使用Brotli算法进行压缩

在.NET Core 2.1中Microsoft引入了Brotli压缩算法的实现。 在性能上,Brotli与DEFLATE和GZIP中使用的算法相似,但是输出的密度大约高20%。

  1. 修改WorkWithCompression方法以使其具有可选参数,以指示是否应使用Brotli并默认情况下使用Brotli,如以下代码中突出显示的内容:
static void WorkWithCompression(bool useBrotli = true)
{
    string fileExt = useBrotli ? "brotli" : "gzip";
    // compress the XML output
    string filePath = Combine(
    CurrentDirectory, $"streams. {fileExt} ");
    FileStream file = File.Create(filePath);
    Stream compressor;
    if (useBrotli)
    {
        compressor = new BrotliStream(file, CompressionMode.Compress);
    }
    else
    {
        compressor = new GZipStream(file, CompressionMode.Compress);
    }
    using (compressor)
    {
        using (XmlWriter xml = XmlWriter.Create(compressor))
        {
            xml.WriteStartDocument();
            xml.WriteStartElement("callsigns");
            foreach (string item in callsigns)
            {
                xml.WriteElementString("callsign", item);
            }
        }
    } // also closes the underlying stream
      // output all the contents of the compressed file
    WriteLine("{0} contains {1:N0} bytes.",
    filePath, new FileInfo(filePath).Length);
    WriteLine(File.ReadAllText(filePath));
    // read a compressed file
    WriteLine("Reading the compressed XML file:");
    file = File.Open(filePath, FileMode.Open);
    Stream decompressor;
    if (useBrotli)
    {
        decompressor = new BrotliStream(
        file, CompressionMode.Decompress);
    }
    else
    {
        decompressor = new GZipStream(
        file, CompressionMode.Decompress);
    }
    using (decompressor)
    {
        using (XmlReader reader = XmlReader.Create(decompressor))
        {
            while (reader.Read())
            {
                // check if we are on an element node named callsign
                if ((reader.NodeType == XmlNodeType.Element)
                && (reader.Name == "callsign"))
                {
                    reader.Read(); // move to the text inside element
                    WriteLine($"{reader.Value}"); // read its value
                }
            }
        }
    }
}
  1. 修改Main方法以两次调WorkWithCompression,一次使用Brotli进行默认设置,一次使用GZIP进行调用,如以下代码所示:
WorkWithCompression();
WorkWithCompression(useBrotli: false);
  1. 运行控制台应用程序,然后比较两个压缩的XML文件的大小。 Brotli的密度大于21%,如以下编辑的输出所示:
/Users/markjprice/Code/Chapter09/WorkingWithStreams/streams.brotli contains 118 bytes.
/Users/markjprice/Code/Chapter09/WorkingWithStreams/streams.gzip contains 150 bytes.

使用管道的高性能流

在.NET Core 2.1中,Microsoft引入了 pipelines 管道。 正确地处理流中的数据需要许多复杂的样板代码,这些代码很难维护。 在本地笔记本电脑上进行测试通常可以使用较小的示例文件,但由于假设不正确,因此在现实世界中无法通过测试。 管道会对此有所帮助。

更多信息:尽管管道在现实世界中功能强大,并且最终您将要了解它们,但是举一个不错的示例会也不是轻松的事,因此我没有在本书中进行介绍的计划。 您可以通过以下链接阅读问题的详细说明以及管道如何提供帮助:https://blogs.msdn.microsoft.com/dotnet/2018/07/09/system-io-pipelines-high-performance-io-in-net/

异步流

Microsoft在C#8.0和.NET Core 3.0引入了流的异步处理。 您将在第13章中了解到这一点。 更多信息:您可以在以下链接上完成有关异步流的教程。 https://docs.microsoft.com/en-us/dotnet/csharp/tutorials/generate-consume-asynchronous-stream