更喜欢使用Stream而不是byte[]

目录

介绍

例子

实现我们自己的批处理

结论


介绍

在处理文件时,通常有两个操作byte[]StreamAPI,所以人们经常选择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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值