目录
介绍
在处理文件时,通常有两个操作byte[]和Stream的API,所以人们经常选择byte[]对应的,因为它需要较少的仪式或直观上更清楚。
你可能认为这个结论很牵强,但我决定在审查和重构一些实际的生产代码后写它。因此,您可能会发现这个简单的技巧在您的代码库中被忽略了,就像我在以前的文章中提到的其他一些简单的事情一样。
例子
让我们看一下像计算文件哈希一样简单的示例。尽管它很简单,但有些人认为唯一的方法是将整个文件读入内存。
有经验的读者可能已经预见到这种方法的问题。让我们看看对900MB文件进行一些基准测试,看看问题是如何表现的,以及我们如何规避它。
基线将是计算来自byte[]源的哈希的朴素解决方案:
public static Guid ComputeHash(byte[] data)
{
using HashAlgorithm algorithm = MD5.Create();
byte[] bytes = algorithm.ComputeHash(data);
return new Guid(bytes);
}
因此,按照文章标题的建议,我们将添加另一个方法,该方法将接受将其Stream转换为字节数组并计算哈希。
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);
}
结果很能说明问题。尽管执行时间非常相似,但内存分配差异很大。
那么这里发生了什么?让我们来看看ComputeHash 实现
public byte[] ComputeHash(Stream inputStream)
{
if (_disposed)
throw new ObjectDisposedException(null);
// Use ArrayPool.Shared instead of CryptoPool because the array is passed out.
byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);
int bytesRead;
int clearLimit = 0;
while ((bytesRead = inputStream.Read(buffer, 0, buffer.Length)) > 0)
{
if (bytesRead > clearLimit)
{
clearLimit = bytesRead;
}
HashCore(buffer, 0, bytesRead);
}
CryptographicOperations.ZeroMemory(buffer.AsSpan(0, clearLimit));
ArrayPool<byte>.Shared.Return(buffer, clearArray: false);
return CaptureHashCodeAndReinitialize();
}
虽然我们试图盲目地遵循文章中的建议,但它没有帮助。从这些数字中得出的关键结论是,使用Stream允许我们以块的形式处理文件,而不是天真地将它们加载到内存中。虽然您可能在小文件上没有注意到这一点,但一旦您必须处理大文件,立即将它们加载到内存中就会变得非常昂贵。
大多数使用byte[]的.NET方法已经表现出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