连接 C# 中的 byte[] 列表

12

我正在创建多个字节数组,需要将它们拼接在一起形成一个大的字节数组 - 我不想使用byte[]数组,但在这里别无选择...

我创建每个字节数组时都会将其添加到列表中,因此只有在获取所有byte[]后才需要进行合并,但我的问题是,实际上最好的方法是什么?

当我有一个包含未知数量byte[]的列表并且我想要将它们全部连接在一起时。

谢谢。

6个回答

25
listOfByteArrs.SelectMany(byteArr=>byteArr).ToArray()

以上代码将把一个字节序列的序列连接成一个序列,并将结果存储在数组中。

虽然可读性较好,但效率不是最大化的 - 它没有利用你已经知道结果字节数组的长度的事实,因此可以避免动态扩展.ToArray()实现,这必然涉及多次分配和数组复制。此外,SelectMany是基于迭代器实现的;这意味着会有大量的接口调用,这相当慢。然而,对于小规模数据集大小来说,这通常不会有太大问题。

如果你需要更快的实现,可以按照以下步骤操作:

var output = new byte[listOfByteArrs.Sum(arr=>arr.Length)];
int writeIdx=0;
foreach(var byteArr in listOfByteArrs) {
    byteArr.CopyTo(output, writeIdx);
    writeIdx += byteArr.Length;
}

或者像马丁诺建议的那样:

var output = new byte[listOfByteArrs.Sum(arr => arr.Length)];
using(var stream = new MemoryStream(output))
    foreach (var bytes in listOfByteArrs)
        stream.Write(bytes, 0, bytes.Length);

一些时间:

var listOfByteArrs = Enumerable.Range(1,1000)
    .Select(i=>Enumerable.Range(0,i).Select(x=>(byte)x).ToArray()).ToList();

在我的机器上,使用短方法将这500500字节连接起来需要15毫秒,而使用快速方法只需要0.5毫秒- YMMV,并且请注意,对于许多应用程序,两种方法都足够快;-)。

最后,您可以使用静态的Array.Copy、底层的Buffer.BlockCopy或预分配的后备缓冲区MemoryStream来替换Array.CopyTo- 在我的测试中它们的性能几乎相同(x64 .NET 4.0)。


4
尽管简洁明了,但是请注意,与传统解决方案相比,该代码非常缓慢。如果速度足够快,那就很好,但可能不够快。 - Eric Lippert
哪一种是“传统解决方案”? - amalgamate
“传统”的解决方案可能是手动嵌套for循环。这大约比基于块复制的解决方案慢三倍,但仍然比“SelectMany”快10倍。 - Eamon Nerbonne

4
这是一种基于Andrew Bezzubfejesjoco的解决方案,预先分配所需的所有内存。这将产生Θ(N)的内存使用和Θ(N)的时间(N是总字节数)。
byte[] result = new byte[list.Sum(a => a.Length)];
using(var stream = new MemoryStream(result))
{
    foreach (byte[] bytes in list)
    {
        stream.Write(bytes, 0, bytes.Length);
    }
}
return result;

2

使用 Linq:

    List<byte[]> list = new List<byte[]>();
    list.Add(new byte[] { 1, 2, 3, 4 });
    list.Add(new byte[] { 1, 2, 3, 4 });
    list.Add(new byte[] { 1, 2, 3, 4 });

    IEnumerable<byte> result = Enumerable.Empty<byte>();

    foreach (byte[] bytes in list)
    {
        result = result.Concat(bytes);
    }

    byte[] newArray = result.ToArray();

也许更快的解决方案是(不预先声明数组):
IEnumerable<byte> bytesEnumerable = GetBytesFromList(list);

byte[] newArray = bytesEnumerable.ToArray();

private static IEnumerable<T> GetBytesFromList<T>(IEnumerable<IEnumerable<T>> list)
{
    foreach (IEnumerable<T> elements in list)
    {
        foreach (T element in elements)
        {
            yield return element;
        }
    }
}

似乎以上方法只会遍历每个数组一次。

2
请注意,这个解决方案在字节数组数量上是O(n^2)的。(你看出来为什么了吗?提示:序列操作符是惰性的。)你可以做得更好。你能找到一个在字节数组数量上是线性的解决方案吗? - Eric Lippert
@Eric:谢谢!对我来说,解决方案的时间复杂度是O(n^2)并不明显。如果我使用单独的方法来形成字节的可枚举集合会怎样呢?我已经更新了答案。 - Andrew Bezzub
懒惰与其有什么关系?非懒惰的实现也将是O(N^2)。相反,您可以拥有一个懒惰的序列和O(N)的性能。问题在于所需的接口包装-您需要连续包装枚举器-您无法声明继续。这并不是完全公平的比较,但是在Haskell中,类似的概念代码将是惰性且O(N)。 - Eamon Nerbonne
确实。现在泛化它。 它不必是List<byte[]>; 它可以是IEnumerable<byte[]>,因为您没有使用List的任何属性,除了它是可枚举的。同样,它不必是byte[]; 您不使用byte或数组的任何属性。这都是序列!在C# 4中,您可以说IEnumerable<T> Flatten<T>(IEnumerable<IEnumerable<T>> items) { foreach(var sequence in items) foreach(T t in sequence) yield return t; } 就完成了。(这取决于IEnumerable<T>的协变性,因此它仅适用于C# 4;有方法使其在C# 3中使用更多类型参数。) - Eric Lippert
1
当然,你可以进一步将其泛化为IEnumerable<R> Flatten<T, R>(IEnumerable<T> items, Func<T, IEnumerable<R>> selector) { foreach(T item in items) foreach(R result in selector(item)) yield return result; },嘿,你刚刚发明了SelectMany;这个方法已经在序列操作库中了。 - Eric Lippert

2

将它们全部写入MemoryStream而不是列表。然后调用MemoryStream.ToArray()。或者当您拥有列表时,首先总结所有字节数组的长度,创建一个新的字节数组,并在大数组中将每个数组复制到最后一个之后。


0

不需要把每个字节数组存储到 List<byte[]> 中,而是可以直接使用 AddRange 方法将它们添加到一个 List<byte> 中。


-1

AddRange方法会将List转换为byte[]吗?不会。 - R. Martinho Fernandes

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