读取和写入文件是一种非常常见的操作。虽然它不是超级复杂,但当我第一次看到 .NET 流时,它们特别令人困惑,这仅仅是因为引擎盖下有一些活动部件。我希望尝试在这篇文章中浮出水面。
对于示例数据,我将引用中世纪阿拉伯学者伊本·泰米亚(Ibn Taymiyya)的一句话。只需将其粘贴到一个简单的文本文件中:
不要太依赖这个世界上的任何人,因为当你处于黑暗中时,即使是你自己的影子也会离开你。
根据我的文件资源管理器,此文件包含价值 115 字节的智慧。要获取这些字节(和文本),您可以像这样阅读它们:
using System.IO;
using System.Text;
var bytes = File.ReadAllBytes(filePath);
var text = Encoding.UTF8.GetString(bytes);
Console.WriteLine(text);
这并不理想,因为没有保险,我们选择了正确的编码。我们可能会得到垃圾文本。因此,邪恶的人Microsoft添加了一个辅助方法,可以最好为我们猜测它。到目前为止,这种方法还没有让我失望。
var text = File.ReadAllText(filePath);
Console.WriteLine(text);
当然,您也可以通过以下方式将内容写入光盘:
File.WriteAllBytes(filePath, bytes);
File.WriteAllText(filePath, text);
这种基本的阅读和写作都很好,但它只对小文件有用。以这种方式读取文件会将其所有内容加载到内存中。如果我们需要将价值数千兆的数据从一个地方移动/复制到另一个地方怎么办?您无法在 2Gb RAM 服务器上执行此操作。而且您可能也不想在普通机器上执行此操作。这是流进来。它们允许您以小块的形式读取/写入文件。
请注意,在这一点上,文件的内容并不重要。我们只想移动/复制一堆字节。在 .NET 中有几个抽象类的现成实现。我的重点将主要放在.其他实现有其用例,但它们遵循相同的模式。
要将文件从 A 点分块复制到 B...好吧,您需要打开源文件,复制几个字节,然后将它们粘贴到目标文件中。回到上次中断的地方,冲洗并重复。
因此,我们需要两个 FileStream 实例。一个用于 src 文件,另一个用于 dest 文件。我们还需要一个_缓冲区。_ 缓冲区是在流之间传输字节块的存储桶。它将是一个数组。它有一个固定的长度,我们将在每次从源流读取时覆盖其内容。这样,我们只为其分配一次内存,并且缓冲区的大小可以最大化每次传输可以加载到内存中的内存量。实现很简单:
void CopyFileInChunks(int chunkSize, string srcFilePath, string destFilePath)
{
// create streams for source and target file
using var reader = new FileStream(srcFilePath, FileMode.Open);
using var writer = new FileStream(destFilePath, FileMode.OpenOrCreate, FileAccess.Write);
// get how much bytes were read from the stream
var readCount = 0;
// the byte bucket to transfer data
var buffer = new byte[chunkSize];
// read bytes from source stream and write them into target stream
while ((readCount = reader.Read(buffer, 0, buffer.Length)) != 0)
{
writer.Write(buffer, 0, readCount);
}
}
由于示例文件只有 115 个字节,因此我们需要使用小 chunkSize。此外,并不是所有流都必须实现接口。这将很快变得相关。IDisposable
我想在这里重点谈两句话:
readCount = reader.Read(buffer, 0, buffer.Length);
// and
writer.Write(buffer, 0, readCount);
只是为了澄清签名和参数:
int Read(buffer, offset, readLength);
// buffer: buffer to store read data,
// offset: buffer offset
// readLength: how many bytes to (try) read into the buffer
// returns: number of bytes that were actually read
void Write(buffer, offset, writeLength);
// buffer: buffer to read data from
// offset: buffer offset
// writeLength: how many bytes write from the buffer
每个流都有 和 属性。由于文件只是一个字节数组,因此长度表示...该数组的长度。Position 属性会跟踪到目前为止读取/写入的字节数,并且会在您调用 和 时自动更新。您可以“手动”设置位置,但在大多数情况下这样做是没有用的。
所以,假设设置为 50。在这种情况下,将有 2 次读取,完全填充缓冲区 (50 + 50 = 100),第三次读取仅剩 15 个字节。这意味着,缓冲区中只会覆盖前 15 个字节。其他位置上的字节仍将保留上次读取时的值,因此不要在最后一次将整个缓冲区内容写入目标流时非常重要。这就是我们转向方法的原因。仅将第一个位置写入目标流。
如果我们将上面的 while 循环修改为:
while ((readCount = reader.Read(buffer, 0, buffer.Length)) != 0)
{
writer.Write(buffer, 0, readCount);
throw new Exception(); // add exception
}
将创建该文件,但不会将任何内容写入其中,尽管调用了方法。但是,如果我们这样写:
while ((readCount = reader.Read(buffer, 0, buffer.Length)) != 0)
{
writer.Write(buffer, 0, readCount);
writer.Flush(); // add Flush() call
throw new Exception();
}
将写入第一次读取。每个流都有自己的内部缓冲区。此缓冲区的大小通常设置为 4096 字节。当它已满时,stream 将自动调用。 方法将内部缓冲区内容写入光盘。通常,最佳做法是不要手动调用它。如果想要更小/更大的内部缓冲区大小,可以在流的构造函数中设置它。Flush()Flush()
另一个重要的细节是流实现接口。 在方法中自动调用。如果您在创建流时省略了 using 语句,除了潜在的内存问题外,您肯定会有一个错误的文件副本。
在我的示例中,我一直使用 50 字节的块大小。这仅用于演示目的。逐字节复制文件也不是很有效。我从来没有真正做过基准测试来测试哪种尺寸是最好的。我想这取决于许多其他事情。安全折衷方案是默认缓冲区大小为 4 KB。
验证内容是否已正确复制非常重要。生成文件校验和的简单方法可以像这样实现:
byte[] GetFileContentHash(string filePath, HashAlgorithm hashAlgorithm)
{
using var file = File.OpenRead(filePath);
var buffer = new byte[file.Length];
file.Read(buffer, 0, (int)file.Length);
return hashAlgorithm.ComputeHash(buffer);
}
就像 Stream 类一样,HashAlgorithm 是一个抽象类。 同样,.NET 有几种算法来实现它。我通常选择使用 sha1 来获得校验和。使用上述方法:
using var sha1 = SHA1.Create();
var hashBytes = GetFileContentHash(filePath, sha1);
就是这样。但是,此实现在较大的文件中存在相同的问题。我们将所有内容加载到内存中。就像流一样,我们可以以块为单位计算文件哈希吗?是的。HashAlgorithm 有两个有用的方法定义:和 。我们可以像这样在块中实现计算校验和:
byte[] GetFileContentHashInChunks(string filePath, HashAlgorithm hashAlgorithm, long chunkSize)
{
using var file = File.OpenRead(filePath);
var readCount = 0;
var buffer = new byte[chunkSize];
while ((readCount = file.Read(buffer, 0, buffer.Length)) != 0)
{
hashAlgorithm.TransformBlock(buffer, 0, readCount, null, 0);
}
hashAlgorithm.TransformFinalBlock(buffer, 0, 0);
return hashAlgorithm.Hash;
}
最后,比较字节是一件微不足道的事情:
public bool CompareHashes(byte[] hash1, byte[] hash2)
{
if(hash1.Length != hash2.Length)
{
return false;
}
return hash1
.Zip(hash2, (a, b) => a == b)
.All(x => x);
}