目录
在审查和重构现实世界的代码库时,我注意到byte[] API是如何被滥用的。这就是为什么在本文中我要分享一些关于为什么不应该在代码中逃避Stream API的想法的原因。
介绍
在处理文件时,通常有两个API同时操作byte[]和Stream,因此人们经常选择byte[]对应的API ,因为它需要较少的仪式或直观地更清晰。
您可能认为这个结论牵强,但我决定在审查和重构一些实际生产代码后写下它。因此,您可能会发现在您的代码库中忽略了这个简单的技巧,就像我在之前的文章中提到的其他一些简单的事情一样。
例子
让我们看一个像计算文件哈希一样简单的例子。尽管它很简单,但有些人认为唯一的方法是将整个文件读入内存。
有经验的读者可能已经预见到这种方法的问题。让我们看看对900MB文件做一些基准测试,看看问题是如何表现出来的,以及我们如何规避它。
基线将是从byte[]源计算哈希的天真解决方案:
public static Guid ComputeHash(byte[] data)
{
using HashAlgorithm algorithm = MD5.Create();
byte[] bytes = algorithm.ComputeHash(data);
return new Guid(bytes);
}
因此,按照文章标题的建议,我们将添加另一个方法,该方法将接受Stream将其转换为byte数组并计算哈希。
public async static Task<Guid> ComputeHash(Stream stream, CancellationToken token)
{
var contents = await ConvertToBytes(stream, token);
return ComputeHash(contents);
}
private static async Task<byte[]> ConvertToBytes(Stream stream, CancellationToken token)
{
using var ms = new MemoryStream();
await stream.CopyToAsync(ms, token);
return ms.ToArray();
}
然而,从byte[]计算哈希并不是唯一的选择。还有一个接受Stream 的重载。让我们使用它。
public static Guid ComputeStream(Stream stream)
{
using HashAlgorithm algorithm = MD5.Create();
byte[] bytes = algorithm.ComputeHash(stream);
stream.Seek(0, SeekOrigin.Begin);
return new Guid(bytes);
}
结果很有说服力:
那么这里发生了什么?虽然我们试图盲目地遵循文章中的建议,但并没有帮助。这些图中的关键要点是使用Stream允许我们以块的形式处理文件,而不是天真地将它们加载到内存中。虽然您可能不会在小文件上注意到这一点,但是一旦您必须处理大文件,立即将它们加载到内存中就会变得非常昂贵。
大多数与.NET一起使用byte[]的方法已经展示了Stream对应方法,因此使用它应该没有问题。当您提供自己的API时,您应该考虑提供一种以强大的逐批方式运行Stream的方法。
让我们使用以下代码检查两个stream是否相等来作为另一个示例:
private const int bufferSize = 2048;
public static bool IsEqual(this Stream stream, Stream otherStream)
{
if (stream is null) return false;
if (otherStream is null) return false;
if (stream.Length != otherStream.Length) return false;
if (stream == otherStream) return true;
byte[] buffer = new byte[bufferSize];
byte[] otherBuffer = new byte[bufferSize];
while (stream.Read(buffer, 0, buffer.Length) > 0)
{
otherStream.Read(otherBuffer, 0, otherBuffer.Length);
if (!otherBuffer.SequenceEqual(buffer))
{
stream.Seek(0, SeekOrigin.Begin);
otherStream.Seek(0, SeekOrigin.Begin);
return false;
}
}
stream.Seek(0, SeekOrigin.Begin);
otherStream.Seek(0, SeekOrigin.Begin);
return true;
}
在这里,我们没有将两个潜在的大文件加载到内存中,而是使用2KB的块来比较它们。一旦块不同,我们就退出。
结论
Stream API允许逐批处理,这使我们能够减少大文件的内存消耗。虽然乍一看,Stream API似乎需要更多的仪式,但它绝对是一个人工具箱中的有用工具。
https://www.codeproject.com/Tips/5308853/Prefer-using-Stream-to-byte