在C#中组合两个或多个字节数组的最佳方法


238

我在C#中有3个字节数组,需要组合成一个。什么是完成此任务的最有效方法?


3
您的具体要求是什么?您是要对数组进行并集还是要保留多个具有相同值的实例?您是否要对项目进行排序,还是要保留初始数组中的顺序?您在寻找速度或代码行中的效率吗?
杰森

喜欢它,“最佳”取决于您的要求。
Ady

7
如果您能够使用LINQ,则可以使用以下Concat方法:IEnumerable<byte> arrays = array1.Concat(array2).Concat(array3);
casperOne 2009年

1
请尝试更清楚地回答您的问题。这个模棱两可的问题已经在足以让您花时间回答您的人们中引起了很多困惑。
德鲁·诺阿克斯

Answers:


326

对于基本类型(包括字节),请使用System.Buffer.BlockCopy代替System.Array.Copy。它更快。

我对每个建议的方法进行了计时,每个循环使用1个字节的3个数组执行了100万次。结果如下:

  1. 新字节数组的使用System.Array.Copy -0.2187556秒
  2. 新的字节数组使用System.Buffer.BlockCopy -0.1406286秒
  3. 使用C#收益运算符的IEnumerable <byte>-0.0781270秒
  4. 使用LINQ的Concat <>的IEnumerable <byte>-0.0781270秒

我将每个数组的大小增加到100个元素,然后重新运行测试:

  1. 新字节数组的使用System.Array.Copy -0.2812554秒
  2. 新字节数组使用System.Buffer.BlockCopy -0.2500048秒
  3. 使用C#收益运算符的IEnumerable <byte>-0.0625012秒
  4. 使用LINQ的Concat <>的IEnumerable <byte>-0.0781265秒

我将每个数组的大小增加到1000个元素,然后重新运行测试:

  1. 使用System.Array.Copy -1.0781457秒的新字节数组
  2. 使用System.Buffer.BlockCopy -1.0156445秒的新字节数组
  3. 使用C#收益运算符的IEnumerable <byte>-0.0625012秒
  4. 使用LINQ的Concat <>的IEnumerable <byte>-0.0781265秒

最后,我将每个数组的大小增加到一百万个元素,然后重新运行测试,每个循环执行4000次:

  1. 新的字节数组使用System.Array.Copy -13.4533833秒
  2. 新的字节数组使用System.Buffer.BlockCopy -13.1096267秒
  3. 使用C#yield运算符的IEnumerable <byte>-0秒
  4. 使用LINQ的Concat <>的IEnumerable <byte>-0秒

因此,如果您需要一个新的字节数组,请使用

byte[] rv = new byte[a1.Length + a2.Length + a3.Length];
System.Buffer.BlockCopy(a1, 0, rv, 0, a1.Length);
System.Buffer.BlockCopy(a2, 0, rv, a1.Length, a2.Length);
System.Buffer.BlockCopy(a3, 0, rv, a1.Length + a2.Length, a3.Length);

但是,如果可以使用IEnumerable<byte>,则肯定会首选LINQ的Concat <>方法。它仅比C#yield操作符慢一点,但更简洁,更优雅。

IEnumerable<byte> rv = a1.Concat(a2).Concat(a3);

如果您具有任意数量的数组,并且正在使用.NET 3.5,则可以使System.Buffer.BlockCopy解决方案更加通用,如下所示:

private byte[] Combine(params byte[][] arrays)
{
    byte[] rv = new byte[arrays.Sum(a => a.Length)];
    int offset = 0;
    foreach (byte[] array in arrays) {
        System.Buffer.BlockCopy(array, 0, rv, offset, array.Length);
        offset += array.Length;
    }
    return rv;
}

*注意:上面的块要求您在顶部添加以下名称空间才能起作用。

using System.Linq;

就乔恩·斯基特(Jon Skeet)关于后续数据结构的迭代(字节数组与IEnumerable <byte>)的观点而言,我重新运行了最后的时序测试(100万个元素,进行4000次迭代),并添加了一个循环,每次循环遍历整个数组通过:

  1. 新的字节数组使用System.Array.Copy -78.20550510秒
  2. 新的字节数组使用System.Buffer.BlockCopy -77.89261900秒
  3. 使用C#收益运算符的IEnumerable <byte>-551.7150161秒
  4. 使用LINQ的Concat <>的IEnumerable <byte>-448.1804799秒

关键是,了解生成的数据结构的创建和使用效率非常重要。仅关注创作的效率可能会忽略与使用相关的效率低下。乔恩·库德斯


61
但是您是否真的根据问题要求将其最终转换为数组?如果没有,那当然会更快-但不能满足要求。
乔恩·斯基特

18
Re:Matt Davis-您的“需求”是否需要将IEnumerable转换为数组并不重要-您所需要的只是在某种程度上实际使用了结果。您对IEnumerable的性能测试之所以如此之低是因为您实际上没有做任何事情!LINQ在您尝试使用结果之前不会执行任何工作。因此,我发现您的答案在客观上是不正确的,并且可能导致其他人在绝对不关心性能的情况下使用LINQ。
csauve

12
我阅读了完整的答案,包括您的更新,我的评论站了起来。我知道我参加晚会很晚,但答案严重误导人,上半场显然是错误的
csauve

14
为什么包含虚假和误导性信息的答案是投票最多的答案,并且在有人(Jon Skeet)指出甚至没有回答OPs问题之后对其进行编辑以基本上完​​全使其原始陈述无效
MrCC 2014年

3
误导性答案。即使是版本也没有回答这个问题。
Serge Profafilecebook 2014年

154

在我看来,许多答案都忽略了上述要求:

  • 结果应该是一个字节数组
  • 它应该尽可能高效

这两个一起排除了一个LINQ字节序列-任何带有yield的都将使得不迭代整个序列就不可能获得最终大小。

如果这些不是真正的要求,那么LINQ可能是一个很好的解决方案(或IList<T>实现)。但是,我假设Superdumbell知道他想要什么。

(编辑:我刚刚想到了另一种方法。制作数组的副本和懒惰地读取它们之间存在很大的语义差异。请考虑在调用Combine(或其他方法之后,如果在“源”数组之一中更改数据,会发生什么情况)方法,但在使用结果之前-使用惰性评估,该更改将是可见的。使用即时复制,则不会显示。不同的情况将要求不同的行为-只是需要注意的事情。)

这是我提议的方法-与其他答案中包含的方法非常相似,当然:)

public static byte[] Combine(byte[] first, byte[] second)
{
    byte[] ret = new byte[first.Length + second.Length];
    Buffer.BlockCopy(first, 0, ret, 0, first.Length);
    Buffer.BlockCopy(second, 0, ret, first.Length, second.Length);
    return ret;
}

public static byte[] Combine(byte[] first, byte[] second, byte[] third)
{
    byte[] ret = new byte[first.Length + second.Length + third.Length];
    Buffer.BlockCopy(first, 0, ret, 0, first.Length);
    Buffer.BlockCopy(second, 0, ret, first.Length, second.Length);
    Buffer.BlockCopy(third, 0, ret, first.Length + second.Length,
                     third.Length);
    return ret;
}

public static byte[] Combine(params byte[][] arrays)
{
    byte[] ret = new byte[arrays.Sum(x => x.Length)];
    int offset = 0;
    foreach (byte[] data in arrays)
    {
        Buffer.BlockCopy(data, 0, ret, offset, data.Length);
        offset += data.Length;
    }
    return ret;
}

当然,“ params”版本需要首先创建一个字节数组数组,这会带来额外的效率低下。


乔恩,我很明白你在说什么。我唯一要说的是,有时会在考虑特定实现的情况下提出问题,而没有意识到存在其他解决方案。仅提供答案而不提供替代方案对我来说似乎是一种伤害。有什么想法吗?
马特·戴维斯

1
@Matt:是的,提供替代方案是很好的-但值得解释的是,它们替代方案,而不是将其作为对所提问题的答案。(我并不是说您这样做了-您的回答很好。)
Jon Skeet

4
(尽管我认为您的性能基准应该显示每种情况下完成所有结果所花费的时间,以避免给懒惰的评估带来不公平的优势。)
Jon Skeet

1
即使不满足“结果必须是数组”的要求,简单地满足“必须在某种情况下使用结果”的要求也会使LINQ并非最佳。我认为应该能够隐含使用结果的要求!
csauve

2
@andleer:除了其他以外,Buffer.BlockCopy仅适用于原始类型。
乔恩·斯基特

44

为了使代码更整洁,我将Matt的LINQ示例更进一步:

byte[] rv = a1.Concat(a2).Concat(a3).ToArray();

就我而言,数组很小,因此我不关心性能。


3
简短的解决方案,性能测试将是很棒的!
塞巴斯蒂安

3
这绝对清晰,易读,不需要任何外部库/帮助程序,并且在开发时间方面非常有效。当运行时性能不是很关键时,这很好。
宾基

28

如果只需要一个新的字节数组,则使用以下命令:

byte[] Combine(byte[] a1, byte[] a2, byte[] a3)
{
    byte[] ret = new byte[a1.Length + a2.Length + a3.Length];
    Array.Copy(a1, 0, ret, 0, a1.Length);
    Array.Copy(a2, 0, ret, a1.Length, a2.Length);
    Array.Copy(a3, 0, ret, a1.Length + a2.Length, a3.Length);
    return ret;
}

另外,如果只需要一个IEnumerable,请考虑使用C#2.0 yield操作符:

IEnumerable<byte> Combine(byte[] a1, byte[] a2, byte[] a3)
{
    foreach (byte b in a1)
        yield return b;
    foreach (byte b in a2)
        yield return b;
    foreach (byte b in a3)
        yield return b;
}

我已经完成了与您的第二个选项类似的操作,以合并大型流,就像一个魅力。:)
Greg D

2
第二个选择很棒。+1。
R. Martinho Fernandes 2009年

10

我实际上在使用Concat时遇到了一些问题...(数组在1000万中,实际上崩溃了)。

我发现以下内容很简单,容易并且可以很好地工作而不会崩溃,并且它适用于任何数量的数组(不只是三个)(它使用LINQ):

public static byte[] ConcatByteArrays(params byte[][]  arrays)
{
    return arrays.SelectMany(x => x).ToArray();
}

6

memorystream类对我来说做得很好。我无法使缓冲区类像内存流一样快地运行。

using (MemoryStream ms = new MemoryStream())
{
  ms.Write(BitConverter.GetBytes(22),0,4);
  ms.Write(BitConverter.GetBytes(44),0,4);
  ms.ToArray();
}

3
作为QWE说,我做了一个测试,一个循环10,000,000次,MemoryStream的出来不是Buffer.BlockCopy慢290%
ESAC

在某些情况下,您可能需要遍历一个可枚举的数组,而无需事先知道各个数组的长度。在这种情况下,这很好。BlockCopy依赖于预先创建目标数组
Sentinel

就像@Sentinel所说,这个答案对我来说是完美的,因为我不知道我必须写的东西的大小,可以让我做得很干净。它也可以与.NET Core 3的[ReadOnly] Span <byte>一起使用!

如果您使用大小的最终大小初始化MemoryStream,将不会重新创建它,并且@esac会更快。
Tono Nam

2
    public static bool MyConcat<T>(ref T[] base_arr, ref T[] add_arr)
    {
        try
        {
            int base_size = base_arr.Length;
            int size_T = System.Runtime.InteropServices.Marshal.SizeOf(base_arr[0]);
            Array.Resize(ref base_arr, base_size + add_arr.Length);
            Buffer.BlockCopy(add_arr, 0, base_arr, base_size * size_T, add_arr.Length * size_T);
        }
        catch (IndexOutOfRangeException ioor)
        {
            MessageBox.Show(ioor.Message);
            return false;
        }
        return true;
    }

不幸的是,这不适用于所有类型。Marshal.SizeOf()将无法返回许多类型的大小(尝试将此方法与字符串数组一起使用,您会看到一个异常“类型'System.String'无法编组为非托管结构;没有有意义的大小或您可以尝试将type参数限制为仅引用类型(通过添加where T : struct),但是-不是CLR内部专家-我不能说您是否也可能在某些结构上获得异常(例如,如果它们包含引用类型字段)。
丹尼尔斯科特

2
    public static byte[] Concat(params byte[][] arrays) {
        using (var mem = new MemoryStream(arrays.Sum(a => a.Length))) {
            foreach (var array in arrays) {
                mem.Write(array, 0, array.Length);
            }
            return mem.ToArray();
        }
    }

如果您对此代码示例做了一些解释,那么答案可能会更好。
AFract 2014年

1
它确实将字节数组的数组连接为一个大字节数组(如下所示):[1,2,3] + [4,5] + [6,7] ==> [1,2,3,4,5 ,6,7]
彼得·埃特尔

1

可以使用泛型来组合数组。以下代码可以轻松扩展为三个数组。这样,您无需为不同类型的数组重复代码。上述一些答案对我来说似乎过于复杂。

private static T[] CombineTwoArrays<T>(T[] a1, T[] a2)
    {
        T[] arrayCombined = new T[a1.Length + a2.Length];
        Array.Copy(a1, 0, arrayCombined, 0, a1.Length);
        Array.Copy(a2, 0, arrayCombined, a1.Length, a2.Length);
        return arrayCombined;
    }

0

这是@Jon Skeet提供的答案的概括。它基本上是相同的,只是它可用于任何类型的数组,而不仅仅是字节:

public static T[] Combine<T>(T[] first, T[] second)
{
    T[] ret = new T[first.Length + second.Length];
    Buffer.BlockCopy(first, 0, ret, 0, first.Length);
    Buffer.BlockCopy(second, 0, ret, first.Length, second.Length);
    return ret;
}

public static T[] Combine<T>(T[] first, T[] second, T[] third)
{
    T[] ret = new T[first.Length + second.Length + third.Length];
    Buffer.BlockCopy(first, 0, ret, 0, first.Length);
    Buffer.BlockCopy(second, 0, ret, first.Length, second.Length);
    Buffer.BlockCopy(third, 0, ret, first.Length + second.Length,
                     third.Length);
    return ret;
}

public static T[] Combine<T>(params T[][] arrays)
{
    T[] ret = new T[arrays.Sum(x => x.Length)];
    int offset = 0;
    foreach (T[] data in arrays)
    {
        Buffer.BlockCopy(data, 0, ret, offset, data.Length);
        offset += data.Length;
    }
    return ret;
}

3
危险!这些方法不适用于元素长度超过一个字节的任何数组类型(字节数组以外的所有内容)。Buffer.BlockCopy()处理的是字节数量,而不是数组元素的数量。它可以轻松地用于字节数组的原因是,数组的每个元素都是一个字节,因此数组的物理长度等于元素的数量。要将John的byte []方法转换为通用方法,您需要将所有偏移量和长度乘以单个数组元素的字节长度-否则,您将不会复制所有数据。
Daniel Scott

2
通常,要进行这项工作,您可以使用计算单个元素的大小,sizeof(...)然后乘以要复制的元素数,但是sizeof不能与泛型类型一起使用。对于某些类型,可以使用Marshal.SizeOf(typeof(T)),但是某些类型(例如,字符串)会出现运行时错误。那些对CLR类型的内部运作有更深入了解的人将能够在这里指出所有可能的陷阱。足以说写通用数组连接方法[使用BlockCopy]并非易事。
丹尼尔·斯科特

2
最后,您可以使用Array.Copy来几乎完全按照上面显示的方式(具有较低的性能)编写一个类似的通用数组连接方法。只需将所有Buffer.BlockCopy调用替换为Array.Copy调用即可。
丹尼尔·斯科特

0
    /// <summary>
    /// Combine two Arrays with offset and count
    /// </summary>
    /// <param name="src1"></param>
    /// <param name="offset1"></param>
    /// <param name="count1"></param>
    /// <param name="src2"></param>
    /// <param name="offset2"></param>
    /// <param name="count2"></param>
    /// <returns></returns>
    public static T[] Combine<T>(this T[] src1, int offset1, int count1, T[] src2, int offset2, int count2) 
        => Enumerable.Range(0, count1 + count2).Select(a => (a < count1) ? src1[offset1 + a] : src2[offset2 + a - count1]).ToArray();

感谢您的贡献。由于十多年前已经有许多对此的高度评价的答案,因此提供一个解释您的方法与众不同的解释会很有用。为什么有人应该使用它而不是例​​如已接受的答案?
Jeremy Caney

我喜欢使用扩展方法,因为有清晰的代码需要理解。该代码选择两个数组,它们的起始索引分别为count和concat。此方法也得到扩展。因此,这适用于所有时间准备就绪的所有阵列类型
MehmetÜNLÜ

这对我来说很有意义!您介意编辑问题以包含该信息吗?我认为这对将来的读者很有价值,因此他们可以快速将您的方法与现有答案区分开。谢谢!
Jeremy Caney

-1

您只需要传递字节数组列表,此函数将为您返回字节数组(合并)。这是我认为最好的解决方案:)。

public static byte[] CombineMultipleByteArrays(List<byte[]> lstByteArray)
        {
            using (var ms = new MemoryStream())
            {
                using (var doc = new iTextSharp.text.Document())
                {
                    using (var copy = new PdfSmartCopy(doc, ms))
                    {
                        doc.Open();
                        foreach (var p in lstByteArray)
                        {
                            using (var reader = new PdfReader(p))
                            {
                                copy.AddDocument(reader);
                            }
                        }

                        doc.Close();
                    }
                }
                return ms.ToArray();
            }
        }

-5

Concat是正确的答案,但由于某种原因,手动操作获得了最多的选票。如果您喜欢这个答案,也许您会更喜欢这个更通用的解决方案:

    IEnumerable<byte> Combine(params byte[][] arrays)
    {
        foreach (byte[] a in arrays)
            foreach (byte b in a)
                yield return b;
    }

这会让您执行以下操作:

    byte[] c = Combine(new byte[] { 0, 1, 2 }, new byte[] { 3, 4, 5 }).ToArray();

5
这个问题专门要求最有效的解决方案。Enumerable.ToArray效率不高,因为它不知道最终数组的大小,而手动滚动技术可以。
乔恩·斯基特
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.