在 .NET 中文件操作方法

作者:微信公众号:【架构师老卢】
6-11 19:9
23

概述:读取和写入文件是一种非常常见的操作。虽然它不是超级复杂,但当我第一次看到 .NET 流时,它们特别令人困惑,这仅仅是因为引擎盖下有一些活动部件。我希望尝试在这篇文章中浮出水面。内容基本阅读/写作流校验基本阅读/写作对于示例数据,我将引用中世纪阿拉伯学者伊本·泰米亚(Ibn Taymiyya)的一句话。只需将其粘贴到一个简单的文本文件中:不要太依赖这个世界上的任何人,因为当你处于黑暗中时,即使是你自己的影子也会离开你。根据我的文件资源管理器,此文件包含价值 115 字节的智慧。要获取这些字节(和文本),您可以像这样阅读它们:using System.IO; using System.Text;

读取和写入文件是一种非常常见的操作。虽然它不是超级复杂,但当我第一次看到 .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 个字节。其他位置上的字节仍将保留上次读取时的值,因此不要在最后一次将整个缓冲区内容写入目标流时非常重要。这就是我们转向方法的原因。仅将第一个位置写入目标流。

Flush()

如果我们将上面的 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 语句,除了潜在的内存问题外,您肯定会有一个错误的文件副本。

Side note (warning)

在我的示例中,我一直使用 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);  
}
阅读排行