关于《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的形式写入底层流。|
写入文本流
让我们输入一些代码来将文本写入流。
- 创建一个名为WorkingWithStreams的新控制台应用程序项目,将其添加到Chapter09工作区,然后选择该项目作为OmniSharp的活动项目。
- 导入System.IO和System.Xml命名空间,并静态导入System.Console,System.Environment和System.IO.Path类型。
- 定义一个可能包含“蝰蛇飞行员”呼号的字符串值数组,并创建一个枚举呼号的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));
}
- 在Main中,调用WorkWithText方法。
- 运行应用程序并查看结果,如下面的输出所示:
/Users/markjprice/Code/Chapter09/WorkingWithStreams/streams.txt contains 60 bytes.
Husker
Starbuck
Apollo
Boomer
Bulldog
Athena
Helo
Racetrack
- 打开创建的文件并检查它是否包含调用符号列表。
写入XML流
编写XML元素有两种方法,如下:
- WriteStartElement和WriteEndElement:当一个元素可能有子元素时,使用此对。
- WriteElementString:当元素没有子元素时使用此元素。
现在,让我们尝试在XML文件中存储相同的字符串值数组。
- 创建一个列举调用符号的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));
}
- 在Main中,注释上一个方法调用,然后将调用添加到WorkWithXml方法。
- 运行应用程序并查看结果,如下面的输出所示:
/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方法。
- 让我们改进与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.");
}
}
}
你也可以返回并修改你之前创建的其他方法,这里作为一个可选练习留给你。
- 运行应用程序并查看结果,如下面的输出所示:
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。
- 导入以下名称空间:
using System.IO.Compression;
- 添加一个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
}
}
}
}
}
- 在Main中,保留对WorkWithXml的调用,并添加对WorkWithCompression的调用,如以下代码所示:
static void Main(string[] args)
{
// WorkWithText();
WorkWithXml();
WorkWithCompression();
}
- 运行控制台应用程序,并比较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%。
- 修改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
}
}
}
}
}
- 修改Main方法以两次调WorkWithCompression,一次使用Brotli进行默认设置,一次使用GZIP进行调用,如以下代码所示:
WorkWithCompression();
WorkWithCompression(useBrotli: false);
- 运行控制台应用程序,然后比较两个压缩的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