【问题标题】:C# byte[] substring? (design)C#字节[]子字符串? (设计)
【发布时间】:2009-12-29 00:55:51
【问题描述】:

我正在将一些文件异步下载到一个大字节数组中,并且每当将一些数据添加到该数组时,我都会定期触发回调。如果我想让开发人员能够使用添加到数组中的最后一块数据,那么……我该怎么做呢?在 C++ 中,我可以给他们一个指向中间某处的指针,然后也许告诉他们在最后一次操作中添加的字节数,这样他们至少知道他们应该查看的块......我真的不知道想给他们该数据的第二个副本,那只是浪费。

我只是想人们是否想在文件完成下载之前处理这些数据。真的有人愿意这样做吗?还是无论如何它是一个无用的功能?当缓冲区(整个字节数组)已满时,我已经有一个回调,然后他们可以转储整个事情而不用担心起点和终点......

【问题讨论】:

  • 马克,你想完成什么?一定有比您使用的“C++”方式更好的方式来完成它。
  • @John:嗯,这就是我要问的。我在问什么是让用户(使用我的库的开发人员)访问刚刚添加到字节数组的数据的最佳方式,以便他们可以在下载完成之前开始处理它(将其写入磁盘,或者...尝试以其他方式开始处理它)。那,以及给用户这个能力是否值得,或者额外的内存使用/处理的开销让人们访问这个能力是值得的。

标签: c# bytearray design-decisions


【解决方案1】:

.NET 有一个结构可以完全满足您的需求:

System.ArraySegment.

在任何情况下,自己实现它也很容易——只需创建一个构造函数,它接受一个基本数组、一个偏移量和一个长度。然后实现一个indexer,在后台偏移索引,这样您的 ArraySegment 就可以无缝地用于代替数组。

【讨论】:

  • 哦,有一个不错的干净解决方案!
【解决方案2】:

你不能给他们一个指向数组的指针,但你可以给他们数组和新数据的起始索引和长度。

但我想知道有人会用它做什么。这是已知的需求吗?或者你只是猜测有一天有人可能想要这个。如果是这样,您是否有任何理由迫不及待地想在某人真正需要该功能时添加它?

【讨论】:

  • YAGNI:你不需要它。 通常,在有明确的用例之前不要向 API 添加功能。以后添加功能很容易——一旦它们加入就删除它们几乎是不可能的。 en.wikipedia.org/wiki/You_ain%27t_gonna_need_it
  • 不,这不是已知的需求。对于我正在处理/创建这个库的项目,我当然不需要它。只是推测某人如何使用它......无论如何,这听起来很合理。谢谢:)
  • 更好的是,使用 System.ArraySegment.
【解决方案3】:

这是否需要取决于您是否有能力在处理文件之前累积文件中的所有数据,或者您是否需要提供一种流模式,以便在每个块到达时对其进行处理。这取决于两件事:有多少数据(您可能不想累积数 GB 的文件),以及文件完全到达需要多长时间(如果您通过慢速链接获取数据,您可能不会希望您的客户等到它全部到达)。因此,根据库的使用方式,添加它是一个合理的特性。流模式通常是一个理想的属性,所以我会投票支持实现该功能。但是,将数据放入数组的想法似乎是错误的,因为它从根本上暗示了非流式设计,并且需要额外的副本。相反,您可以做的是将每一块到达的数据保留为离散的部分。这些可以存储在一个容器中,最后添加和从前面移除是有效的。

【讨论】:

  • 我正在为HttpWebRequest 构建一个包装器(WebClient 并不能满足我的所有需求)。据我所知,我可以对其执行的唯一异步读取操作 (BeginRead) 将数据放入 byte[] 数组中(对我来说这是一种足够好的格式)。 大部分时间我确实通过查看ContentLength 标头知道数组的最终大小,以便我可以适当地分配内存。但是,如果文件很大,我已经指定了一个上限,此时数据可以转储到文件中(我已经实现了“缓冲区满”回调)。
  • ...无论如何,我不认为将它写入字节数组真的是低效的;我不太确定它可以更有效地完成。为什么我仍然想要离散的、随机大小的数据块?那将更难处理。正如其他人所建议的那样,使用ArraySegment,开发人员可以在每个块到达时对其进行处理,或者等待整个文件并对整个数组执行任何他们想要的操作,而不必在事后尝试将它们组合在一起。
【解决方案4】:

复制一大块字节数组可能看起来“浪费”,但话说回来,像 C# 这样的面向对象语言往往比过程语言更浪费一点。一些额外的 CPU 周期和一点额外的内存消耗可以大大降低开发过程的复杂性并增加灵活性。事实上,在我看来,将字节复制到内存中的新位置听起来是不错的设计,而不是指针方法,它会让其他类访问私有数据。

但如果你确实想使用指针,C# 确实支持它们。 Here is a decent-looking tutorial.作者说得对,“...指针只有在执行速度非常重要的 C# 中才真正需要。”

【讨论】:

    【解决方案5】:

    我同意 OP:有时您只是需要注意效率。我不认为提供 API 的例子是最好的,因为这肯定需要安全和简单而不是效率。

    然而,一个简单的例子是在处理大量包含无数记录的巨大二进制文件时,例如在编写解析器时。如果不使用诸如 System.ArraySegment 之类的机制,解析器就会变成一个大内存占用者,并且会通过创建数不胜数的新数据元素、复制所有内存以及从堆中分片来大大减慢速度。这是一个非常现实的性能问题。我一直在为电信行业编写此类解析器,这些解析器每天在多个类别中的每个类别中生成数百万条记录,这些记录来自许多具有可变长度二进制结构的交换机,需要被解析到数据库中。

    使用 System.ArraySegment 机制与为每条记录创建新的结构副本相比,极大地加快了解析速度,并大大降低了解析器的峰值内存消耗。这些都是非常实际的优势,因为服务器运行多个解析器,经常运行它们,并且速度和内存节省 = 非常实际的成本节省,因为不必有这么多专用于解析的处理器。

    System.Array 段非常好用。这是一个简单的示例,它提供了一种基本方法来跟踪一个典型的大二进制文件中的各个记录,该文件充满了具有固定长度标题和可变长度记录大小的记录(明显的异常控制已删除):

    public struct MyRecord
    {
        ArraySegment<byte> header;
        ArraySegment<byte> data;
    }
    
    
    public class Parser
    {
        const int HEADER_SIZE = 10;
        const int HDR_OFS_REC_TYPE = 0;
        const int HDR_OFS_REC_LEN = 4;
        byte[] m_fileData;
        List<MyRecord> records = new List<MyRecord>();
    
        bool Parse(FileStream fs)
        {
            int fileLen = (int)fs.FileLength;
            m_fileData = new byte[fileLen];
            fs.Read(m_fileData, 0, fileLen);
            fs.Close();
            fs.Dispose();
            int offset = 0;
            while (offset + HEADER_SIZE < fileLen)
            {
                int recType = (int)m_fileData[offset];
                switch (recType) { /*puke if not a recognized type*/ }
                int varDataLen = ((int)m_fileData[offset + HDR_OFS_REC_LEN]) * 256
                         + (int)m_fileData[offset + HDR_OFS_REC_LEN + 1];
                if (offset + varDataLen > fileLen) { /*puke as file has odd bytes at end*/}
                MyRecord rec = new MyRecord();
                rec.header = new ArraySegment(m_fileData, offset, HEADER_SIZE);
                rec.data = new ArraySegment(m_fileData, offset + HEADER_SIZE,   
                              varDataLen);
                records.Add(rec);
                offset += HEADER_SIZE + varDataLen;
            } 
        }
    }
    

    上面的示例为您提供了一个列表,其中包含文件中每条记录的 ArraySegments,同时将所有实际数据保留在每个文件的一个大数组中。唯一的开销是 MyRecord 结构中每条记录的两个数组段。在处理记录时,您有 MyRecord.header.Array 和 MyRecord.data.Array 属性,它们允许您对每条记录中的元素进行操作,就好像它们是它们自己的 byte[] 副本一样。

    【讨论】:

      【解决方案6】:

      我认为你不应该打扰。

      为什么会有人想要使用它?

      【讨论】:

      • 你知道,我从理论的角度考虑过这个问题,也许你也是,但从现实的角度来看你是对的......这个星球上没有另一个灵魂会离开无论如何都要使用这个库。但是出于好奇,你是说它不是一个非常有用的功能,还是我刚才所说的?
      • 我觉得一开始没什么用。
      【解决方案7】:

      听起来你想要event。

      public class ArrayChangedEventArgs : EventArgs {
          public (byte[] array, int start, int length) {
              Array = array;
              Start = start;
              Length = length;
          }
          public byte[] Array { get; private set; }
          public int Start { get; private set; }
          public int Length { get; private set; }
      }
      
      // ...
      // and in your class:
      
      public event EventHandler<ArrayChangedEventArgs> ArrayChanged;
      
      protected virtual void OnArrayChanged(ArrayChangedEventArgs e)
      {
          // using a temporary variable avoids a common potential multithreading issue
          // where the multicast delegate changes midstream.
          // Best practice is to grab a copy first, then test for null
      
          EventHandler<ArrayChangedEventArgs> handler = ArrayChanged;
      
          if (handler != null)
          {
              handler(this, e);
          }
      }
      
      // finally, your code that downloads a chunk just needs to call OnArrayChanged()
      // with the appropriate args
      

      客户端挂钩事件并在事情发生变化时被调用。这是 .NET 中的大多数客户端代码期望在 API 中的内容(“当有事情发生时给我打电话”)。他们可以通过以下简单的方式连接到代码中:

      yourDownloader.ArrayChanged += (sender, e) =>
          Console.WriteLine(String.Format("Just downloaded {0} byte{1} at position {2}.",
                  e.Length, e.Length == 1 ? "" : "s", e.Start));
      

      【讨论】:

      • Err...我已经确实有一个事件,我在询问在该事件期间向用户发送数据的最佳格式。即事件e 应该包含什么?
      猜你喜欢
      • 2012-06-06
      • 2016-06-28
      • 1970-01-01
      • 2019-02-01
      • 2021-01-03
      • 1970-01-01
      • 1970-01-01
      • 2023-04-06
      • 1970-01-01
      相关资源
      最近更新 更多