C# byte[] substring? (design)

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

我只是在想,如果有人想在文件下载完成之前处理这些数据。任何人真正需要这样做吗?或者这只是一个无用的功能?我已经为缓冲区(整个字节数组)填满时设置了一个回调函数,然后他们可以将整个缓冲区倾倒出来,而不必担心起始和结束点...


马克,你想要实现什么目标?一定有比你正在使用的"C++"方式更好的方法来实现它。 - John Saunders
@John:嗯,这就是我在问的。我想知道最好的方法是什么,可以让用户(使用我的库的开发人员)访问刚刚添加到字节数组中的数据,以便他们可以在下载完成之前开始处理它(将其写入磁盘或尝试以其他方式开始处理它)。此外,是否值得给用户这种能力,或者为了让人们访问它而增加额外的内存使用/处理负担是否值得。 - mpen
7个回答

11

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

System.ArraySegment

无论如何,您也可以轻松地自己实现它-只需创建一个构造函数,它接受一个基本数组、一个偏移量和一个长度。然后实现一个索引器,在幕后偏移索引,这样您的ArraySegment就可以无缝地用于替代数组。


哦,好的,有一个漂亮干净的解决方案! - mpen

3

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

但我不得不想知道有人会用这个做什么。这是一个已知的需求吗?还是你只是猜测有一天可能会有人需要它。如果是这样的话,有没有任何理由为什么你不能等到有人真正需要它时再添加这个功能呢?


1
YAGNI:你不需要它。通常情况下,在明确需要之前不要向API添加功能。稍后添加功能很容易,但一旦添加进去就几乎不可能删除它们。http://en.wikipedia.org/wiki/You_ain%27t_gonna_need_it - LBushkin
不,这不是一个已知的需求。我肯定在我正在工作/创建这个库的项目中不需要它。只是在推测有人可能会如何使用它...无论如何,这听起来很合理。谢谢 :) - mpen
2
最好使用 System.ArraySegment<byte>。 - Trillian

1

我同意原帖的观点:有时候你确实需要关注效率。我认为提供API的例子并不是最好的,因为这当然需要在安全性和简单性方面倾向于效率。

然而,一个简单的例子是处理大量具有无数记录的巨大二进制文件,例如编写解析器时。如果不使用System.ArraySegment等机制,解析器将成为一个大内存占用者,并且通过创建无数新数据元素、复制所有内存并且使堆碎片化来大大减慢速度。这是一个非常真实的性能问题。我经常为电信设备编写这些类型的解析器,每天从多个交换机中生成数百万条记录,每个交换机都有多个类别的可变长度二进制结构需要解析到数据库中。

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

System.Array segment非常容易使用。这里有一个简单的例子,提供了一种基本的方法来跟踪典型的大型二进制文件中的每个记录,该文件包含一个固定长度的头部和可变长度的记录大小(明显的异常控制已删除)。
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[]副本一样操作每个记录中的元素。

1

复制字节数组的一部分可能看起来有些“浪费”,但是面向对象的语言(如C#)通常比过程式语言更加浪费。在开发过程中,多花一些CPU周期和一点额外的内存消耗可以大大降低复杂性并增加灵活性。实际上,将字节复制到内存中的新位置对我来说听起来像是很好的设计,而不是指针方法,这将使其他类可以访问私有数据。

但是,如果您确实想使用指针,C#也支持它们。这里有一个看起来不错的教程。 作者在他陈述“...只有在C#中执行速度非常重要时才真正需要指针”时是正确的。


1

是否需要这个功能取决于您是否能够在处理数据之前累积文件中的所有数据,或者您是否需要提供流模式,在此模式下,您可以在到达每个块时处理它。这取决于两件事:有多少数据(您可能不想累积多个千兆字节的文件),以及文件完全到达需要多长时间(如果您通过缓慢的链接获取数据,则可能不希望客户端等待直到所有数据都到达)。因此,根据库的使用方式,添加此功能是合理的。流模式通常是一个理想的属性,因此我建议实现此功能。但是,将数据放入数组的想法似乎是错误的,因为它从根本上意味着非流设计,并且需要额外的复制。相反,您可以将到达的每个数据块保留为单独的块。这些可以存储在容器中,对于在末尾添加和从前面删除是有效的。


我正在为HttpWebRequest构建一个包装器(WebClient无法满足我的所有需求)。据我所知,我可以在其中执行的唯一异步读取操作(BeginRead)将数据放入byte[]数组中(对我来说,这是一个足够好的格式)。大多数情况下,我通过查看ContentLength标头来知道数组的最终大小,因此我可以适当地分配内存。但是,如果文件很大,我已经指定了一个上限,在达到该上限时,数据可以转储到文件中(我已经实现了“缓冲区满”回调)。 - mpen
无论如何,我不认为将其写入字节数组真的是低效的;我也不确定它是否能够更加高效地完成。为什么我想要离散、随机大小的数据块呢?那会让工作变得更加困难。像其他人建议的那样使用“ArraySegment”,开发人员可以在每个块到达时处理它,或者等待整个文件并对完整数组进行任何想要的操作,而无需在后面尝试将其组合在一起。 - mpen

0

这听起来像是你想要一个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));

抱歉...我已经有一个事件了,我想知道在该事件期间向用户发送数据的最佳格式是什么。即,事件“e”应包含什么? - mpen

0

我认为你不应该费心。

天哪,为什么有人想要使用它呢?


1
你知道吗,我从理论的角度考虑过这个问题,也许你也是,但从实际的角度来看,你是对的......在这个星球上没有其他人会使用这个库。但出于好奇,你是指它不是一个非常有用的功能,还是我刚才说的话? - mpen
我认为这一点起初并不有用。 - SLaks

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接