C#byte [] substring? (设计)

时间:2009-12-29 00:55:51

标签: c# bytearray design-decisions

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

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

7 个答案:

答案 0 :(得分:11)

.NET有一个完全符合你想要的结构:

System.ArraySegment

在任何情况下,也很容易自己实现它 - 只需创建一个带有基本数组,偏移量和长度的构造函数。然后实现一个indexer来抵消幕后的索引,这样你的ArraySegment可以无缝地用在数组的位置。

答案 1 :(得分:3)

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

但是我不得不怀疑有人会用这个。这是一个已知的需求吗?或者你只是猜测某人有一天会想要这个。如果是这样,有没有理由为什么一旦真人需要它就等不及添加功能呢?

答案 2 :(得分:1)

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

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

答案 3 :(得分:1)

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

答案 4 :(得分:1)

我同意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 []副本一样。

答案 5 :(得分:0)

我认为你不应该打扰。

为什么有人想要使用它?

答案 6 :(得分: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));