【问题标题】:Best way to combine two or more byte arrays in C#在 C# 中组合两个或多个字节数组的最佳方法
【发布时间】:2010-09-29 17:46:32
【问题描述】:

我在 C# 中有 3 个字节数组,我需要将它们合并为一个。完成这项任务最有效的方法是什么?

【问题讨论】:

  • 您的具体要求是什么?您是在合并数组还是保留相同值的多个实例?您想要对项目进行排序,还是想要保留初始数组中的顺序?您是在寻找速度还是代码行数的效率?
  • 如果你能够使用LINQ,那么你可以使用Concat方法:IEnumerable<byte> arrays = array1.Concat(array2).Concat(array3);
  • 请尽量让您的问题更清楚。这个含糊不清的问题在那些愿意花时间回答你的好人中引起了很多困惑。

标签: c# arrays


【解决方案1】:

对于原始类型(包括字节),使用System.Buffer.BlockCopy 而不是System.Array.Copy。它更快。

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

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

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

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

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

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

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

  1. 使用 System.Array.Copy 的新字节数组 - 13.4533833 秒
  2. 使用 System.Buffer.BlockCopy 的新字节数组 - 13.1096267 秒
  3. IEnumerable 使用 C# yield 运算符 - 0 秒
  4. IEnumerable 使用 LINQ 的 Concat - 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)的迭代的观点,我重新运行了最后的时序测试(100 万个元素,4000 次迭代),添加了一个循环来迭代整个每次传递的数组:

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

关键是,了解生成的数据结构的创建和使用的效率非常很重要。仅仅关注创造的效率可能会忽略与使用相关的低效率。致敬,乔恩。

【讨论】:

  • 但是您实际上是否按照问题要求将其转换为最后的数组?如果不是,当然它会更快 - 但它不满足要求。
  • Re:Matt Davis - 你的“要求”是否需要将 IEnumerable 转换为数组并不重要 - 你的要求所需要的只是结果实际上是用于某种时尚。您对 IEnumerable 的性能测试如此之低的原因是您实际上并没有做任何事情!在您尝试使用结果之前,LINQ 不会执行任何工作。出于这个原因,我发现您的回答在客观上是不正确的,并且可能会导致其他人在他们绝对不应该使用 LINQ 的情况下(如果他们关心性能)。
  • 我阅读了整个答案,包括您的更新,我的评论是正确的。我知道我迟到了,但答案严重误导,前半部分明显错误
  • 为什么包含虚假和误导性信息的答案是票数最高的答案,并且在有人 (Jon Skeet) 指出后被编辑为基本上完全使其原始陈述无效它甚至没有回答 OP 的问题?
  • 误导性答案。甚至版本也没有回答这个问题。
【解决方案2】:

在我看来,许多答案都忽略了规定的要求:

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

这两个一起排除了 LINQ 字节序列 - 任何带有 yield 的东西都会使得如果不遍历整个序列就无法获得最终大小。

当然,如果这些不是真正的要求,LINQ 可能是一个非常好的解决方案(或IList&lt;T&gt; 实现)。不过,我假设 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”版本需要先创建一个字节数组,这会带来额外的低效率。

【讨论】:

  • 乔恩,我明白你在说什么。我唯一的一点是,有时会在考虑到特定实现的情况下提出问题,而没有意识到存在其他解决方案。简单地提供答案而不提供替代方案对我来说似乎是一种伤害。想法?
  • @Matt:是的,提供替代品很好——但值得解释的是,它们替代品,而不是把它们作为所提问题的答案。 (我不是说你这样做了——你的回答非常好。)
  • (虽然我认为你的性能基准应该显示在每种情况下通过所有结果所花费的时间,以避免给惰性评估带来不公平的优势。)
  • 即使不满足“结果必须是数组”的要求,仅仅满足“结果必须以某种方式使用”的要求也会导致 LINQ 非最优。我认为能够使用结果的要求应该是隐含的!
  • @andleer:除此之外,Buffer.BlockCopy 仅适用于原始类型。
【解决方案3】:

我将 Matt 的 LINQ 示例进一步简化了代码:

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

就我而言,数组很小,所以我不关心性能。

【讨论】:

  • 简短的解决方案,性能测试会很棒!
  • 这绝对是清晰、易读的,不需要外部库/帮助程序,并且在开发时间方面非常有效。当运行时性能不重要时非常有用。
【解决方案4】:

如果您只需要一个新的字节数组,请使用以下内容:

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;
}

【讨论】:

  • 我已经做了类似于你的第二个选项来合并大流,就像一个魅力。 :)
  • 第二个选项很棒。 +1。
【解决方案5】:

我实际上在使用 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();
    }
    

    【讨论】:

    • 正如 qwe 所说,我在循环中进行了 10,000,000 次测试,结果 MemoryStream 比 Buffer.BlockCopy 慢 290%
    • 在某些情况下,您可能会在不知道各个数组长度的情况下迭代可枚举的数组。这在这种情况下效果很好。 BlockCopy 依赖于预先创建的目标数组
    • 正如@Sentinel 所说,这个答案对我来说是完美的,因为我不知道我必须写的东西的大小,并且让我做事非常干净。它还与 .NET Core 3 的 [ReadOnly]Span! 配合得很好
    • 如果使用最终大小的大小初始化 MemoryStream,它将不会被重新创建,并且会更快@esac。
    【解决方案7】:
        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();
            }
        }
    

    【讨论】:

    • 如果您发布了一些关于此代码示例的说明,您的答案可能会更好。
    • 它确实将一个字节数组的数组连接成一个大字节数组(像这样):[1,2,3] + [4,5] + [6,7] ==> [1 ,2,3,4,5,6,7]
    【解决方案8】:
        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() 将无法返回许多类型的大小(尝试将此方法与字符串数组一起使用,您将看到异常“Type 'System.String' cannot be marshaled as an unmanaged structure;没有有意义的大小或偏移量可以计算”。您可以尝试将类型参数限制为仅引用类型(通过添加 where T : struct),但是 - 不是 CLR 内部的专家 - 我不能说你是否会在某些情况下遇到异常结构也是如此(例如,如果它们包含引用类型字段)。
    【解决方案9】:

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

    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;
        }
    

    【讨论】:

      【解决方案10】:
          /// <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();
      

      【讨论】:

      • 感谢您的贡献。由于十多年前已经有许多对此评价很高的答案,因此解释一下您的方法的区别是很有用的。为什么有人应该使用它而不是例​​如接受的答案?
      • 我喜欢使用扩展方法,因为代码清晰易懂。此代码选择两个具有起始索引和计数和连接的数组。这种方法也扩展了。所以,这适用于随时准备好的所有数组类型
      • 这对我来说很有意义!您介意编辑您的问题以包含该信息吗?我认为这对未来的读者来说很有价值,这样他们就可以快速区分你的方法和现有的答案。谢谢!
      【解决方案11】:

      这是@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;
      }
      

      【讨论】:

      • 危险!这些方法不适用于任何元素长度超过一个字节的数组类型(几乎除了字节数组之外的所有内容)。 Buffer.BlockCopy() 适用于字节数量,而不是数组元素的数量。它可以很容易地与字节数组一起使用的原因是数组的每个元素都是一个字节,因此数组的物理长度等于元素的数量。要将 John 的 byte[] 方法转换为通用方法,您需要将所有偏移量和长度乘以单个数组元素的字节长度 - 否则您将不会复制所有数据。
      • 为了完成这项工作,您通常会使用 sizeof(...) 计算单个元素的大小,然后将其乘以要复制的元素数量,但 sizeof 不能与泛型一起使用类型。可以 - 对于某些类型 - 使用 Marshal.SizeOf(typeof(T)),但某些类型(例如字符串)会出现运行时错误。对 CLR 类型的内部工作有更透彻了解的人将能够在此处指出所有可能的陷阱。可以说[使用 BlockCopy] 编写通用数组连接方法并非易事。
      • 最后 - 您可以使用 Array.Copy 以几乎完全如上所示的方式编写这样的通用数组连接方法(性能略低)。只需将所有 Buffer.BlockCopy 调用替换为 Array.Copy 调用即可。
      【解决方案12】:

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

      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();
                  }
              }
      

      【讨论】:

        【解决方案13】:

        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();
        

        【讨论】:

        • 该问题专门要求最有效的解决方案。 Enumerable.ToArray 不会非常有效,因为它无法知道最终数组的大小开始 - 而手动技术可以。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-11-10
        • 2017-12-08
        • 2015-10-21
        • 1970-01-01
        • 2010-11-15
        相关资源
        最近更新 更多