Найкращий спосіб поєднати два або більше байтових масивів у C #


238

У мене є 3 байтові масиви в C #, які мені потрібно об'єднати в один. Який був би найефективніший метод для виконання цього завдання?


3
Які конкретно ваші вимоги? Ви берете об'єднання масивів чи зберігаєте кілька примірників одного і того ж значення? Ви хочете, щоб елементи були відсортовані, чи ви хочете зберегти впорядкування у початкових масивах? Ви шукаєте ефективність у швидкості чи в рядках коду?
— ясон

Любіть це, "найкраще" залежить від того, які ваші вимоги.
— Аді

7
Якщо ви можете використовувати LINQ, то ви можете просто скористатися Concatметодом:IEnumerable<byte> arrays = array1.Concat(array2).Concat(array3);
— casperOne

1
Будь ласка, спробуйте бути більш чіткими у своїх питаннях. Це розпливчасте запитання викликало багато плутанини серед людей, які досить добре знайшли час, щоб відповісти вам.
— Дрю Ноакс

Відповіді:


326

Для примітивних типів (включаючи байти) використовуйте System.Buffer.BlockCopyзамість System.Array.Copy. Це швидше.

Я приуротив кожен із запропонованих методів у циклі, виконаному 1 мільйон разів, використовуючи 3 масиви по 10 байт кожен. Ось результати:

  1. Використання System.Array.Copy нового байтового масиву - 0,2187556 секунд
  2. Використання System.Buffer.BlockCopy нового байтового масиву - 0,1406286 секунд
  3. Численні <байти> з використанням оператора виходу C # - 0,0781270 секунд
  4. Численні <байти>, використовуючи Concat LINQ <> - 0,0781270 секунд

Я збільшив розмір кожного масиву до 100 елементів і повторно запустив тест:

  1. Використовуючи новий байтовий масив System.Array.Copy - 0,2812554 секунди
  2. Використовуючи новий байтовий масив System.Buffer.BlockCopy - 0,2500048 секунд
  3. Численні <байти> з використанням оператора виходу C # - 0,0625012 секунд
  4. Численні <байти> з використанням Concat LINQ <> - 0,0781265 секунд

Я збільшив розмір кожного масиву до 1000 елементів і повторно провів тест:

  1. Використання System.Array.Copy нового байтового масиву - 1.0781457 секунд
  2. Використовується новий байтовий масив System.Buffer.BlockCopy - 1,0156445 секунд
  3. Численні <байти> з використанням оператора виходу C # - 0,0625012 секунд
  4. Численні <байти> з використанням Concat LINQ <> - 0,0781265 секунд

Нарешті, я збільшив розмір кожного масиву до 1 мільйона елементів і повторно запустив тест, виконавши кожен цикл лише 4000 разів:

  1. Використання System.Array.Copy нового байтового масиву - 13.4533833 секунд
  2. Використання System.Buffer.BlockCopy нового байтового масиву - 13.1096267 секунд
  3. Численні <байти> за допомогою оператора виходу C # - 0 секунд
  4. Численні <байти> з використанням Concat <> - 0 секунд LINQ

Отже, якщо вам потрібен новий байтовий масив, використовуйте

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>, НАДАГАЛЬНО віддайте перевагу методу Concat <> LINQ. Це лише трохи повільніше, ніж оператор виходу C #, але є більш стислим та елегантним.

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;

На думку Джона Скіта щодо ітерації наступних структур даних (масив байтів проти IEnumerable <байт>), я повторно провів останній тест хронометражу (1 мільйон елементів, 4000 ітерацій), додавши цикл, який повторює повний масив з кожним пропуск:

  1. Використовуючи новий байтовий масив System.Array.Copy - 78.20550510 секунд
  2. Використання System.Buffer.BlockCopy нового байтового масиву - 77,89261900 секунд
  3. Численні <байти> з використанням оператора виходу C # - 551,7150161 секунди
  4. Численні <байти> з використанням Concat <> - 448.1804799 секунд LINQ

Справа в тому, що ДУЖЕ важливо розуміти ефективність як створення, так і використання отриманої структури даних. Просто зосередження уваги на ефективності створення може не помітити неефективність, пов'язану з використанням. Кудос, Джон.


61
Але ви насправді перетворюєте його в масив наприкінці, як це вимагає питання? Якщо ні, звичайно, це швидше - але це не відповідає вимогам.
— Джон Скіт

18
Re: Метт Девіс - Не має значення, чи потрібно "вашим" вимогам перетворити IEnumerable в масив - все, що потрібно вашим вимогам, полягає в тому, що результат насправді використовується в певній частині . Причина, по якій ваші тести на ефективність на IEnumerable такі низькі, полягає в тому, що ви насправді нічого не робите ! LINQ не виконує жодної своєї роботи, поки ви не спробуєте використати результати. З цієї причини я вважаю вашу відповідь об'єктивно неправильною і можу змусити інших використовувати LINQ, коли вони абсолютно не повинні, якщо вони піклуються про продуктивність.
— csauve

12
Я прочитав всю відповідь, включаючи ваше оновлення, мій коментар стоїть. Я знаю, що я приєднуюся до партії пізно, але відповідь грубо хибний, і перша половина явно хибна .
— csauve

14
Чому відповідь, що містить помилкову та оманливу інформацію, є голосовою відповіддю і її редагували в основному повністю недійсною її первісною заявою після того, як хтось (Джон Скіт) вказав, що навіть не відповів на питання ОП?
— MrCC

3
Неправильна відповідь. Навіть видання не відповідає на запитання.
— Сергій Профафілебудник

154

Багато відповідей, як мені здається, ігнорують заявлені вимоги:

  • Результатом повинен бути байтовий масив
  • Він повинен бути максимально ефективним

Ці два разом виключають послідовність байтів LINQ - все з yieldцим унеможливить отримання остаточного розміру, не повторюючи всю послідовність.

Якщо це, звичайно, не реальні вимоги, LINQ може бути ідеально хорошим рішенням (або IList<T>реалізацією). Однак я припускаю, що Супердумбелл знає, чого хоче.

(EDIT: У мене щойно була думка. Існує велика семантична різниця між створенням копії масивів і лінивим їх читанням. Поміркуйте, що станеться, якщо ви зміните дані в одному з "джерельних" масивів після виклику 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: Так, пропонувати альтернативи - це добре, але варто пояснити, що це альтернативи, а не передавати їх як відповідь на запитання. (Я не кажу, що ви це зробили - ваша відповідь дуже хороша.)
— Джон Скіт

4
(Хоча я думаю, що ваш показник ефективності повинен показувати час, необхідний для проходження всіх результатів у кожному конкретному випадку, щоб уникнути нечесної переваги лінивій оцінці.)
— Джон Скіт

1
Навіть не дотримуючись вимоги "результат повинен бути масив", просто виконання вимоги "результат повинен бути використаний у певній фазі" зробить LINQ неоптимальним. Я думаю, що вимога вміти використовувати результат має бути неявною!
— csauve

2
@andleer: Окрім всього іншого, Buffer.BlockCopy працює лише з примітивними типами.
— Джон Скіт

44

Я зробив приклад Метта 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:

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

Я зробив щось подібне до вашого другого варіанту, щоб об'єднати великі потоки, працював як шарм. :)
— Грег Д

2
Другий варіант - чудовий. +1.
— Р. Мартіньо Фернандес

11

Я фактично зіткнувся з деякими проблемами з використанням Concat ... (з масивами в 10 мільйонів він фактично вийшов з ладу).

Я знайшов таке, що є простим, легким і працює досить добре, не врізавшись у мене, і він працює для БУДЬ-якої кількості масивів (не лише трьох) (для цього використовується LINQ):

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

6

Клас потоку пам’яті робить цю роботу для мене досить непогано. Я не міг змусити клас буфера працювати так швидко, як потік пам'яті.

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 вийшов на 290% повільніше, ніж Buffer.BlockCopy
— esac

У деяких випадках ви можете повторювати безліч масивів без будь-якого передбачення довжин окремих масивів. Це добре працює в цьому сценарії. BlockCopy розраховує на попередньо створений масив призначення
— Sentinel

Як сказав @Sentinel, ця відповідь ідеально підходить мені, оскільки я не знаю розміру речей, які я повинен писати, і дозволяє мені робити дуже чітко. Він також чудово грає з програмою [ReadOnly] .NET Core 3 на <Baite>!
— Вода

Якщо ви ініціалізуєте MemoryStream з кінцевим розміром розміру, він не буде відтворений, і він буде швидше @esac.
— Тоно Нам

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 "не може бути розміщений як некерована структура; немає значущого розміру або зміщення можна обчислити ". Ви можете спробувати обмежити параметр типу лише типовими посиланнями (додавши 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

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 () працює з кількістю байтів, а не кількістю елементів масиву. Причина, за якою легко використовувати байтовий масив, полягає в тому, що кожен елемент масиву - це один байт, тому фізична довжина масиву дорівнює кількості елементів. Щоб перетворити байтові [] методи Джона в загальні методи, вам потрібно буде множити всі зміщення та довжини на довжину байтів одного елемента масиву - інакше ви не скопіюєте всі дані.
— Даніель Скотт

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

Дякую за внесок. Оскільки вже існує низка високо оцінених відповідей на це питання, що пройшли понад десятиліття тому, було б корисно запропонувати пояснення того, що відрізняє ваш підхід. Чому хтось повинен використовувати це замість, наприклад, прийнятої відповіді?
— Джеремі Кейні

Мені подобається використовувати розширені методи, тому що є чіткий код для розуміння. Цей код вибирає два масиви із початковим індексом, а також підрахунком та концетом. Також цей метод розширений. Отже, це для всіх типів масивів, готових на всі часи
— Мехмет ЮНЛЮ

Це має для мене сенс! Ви не хочете редагувати своє запитання, щоб включити цю інформацію? Я думаю, що майбутнім читачам було б корисно мати це наперед, щоб вони могли швидко відрізнити ваш підхід від існуючих відповідей. Дякую!
— Джеремі Кейні

-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
Питання конкретно задає найбільш ефективне рішення. Перелічувальний.ToArray не буде дуже ефективним, тому що він не може знати розмір остаточного масиву для початку - тоді як ручні прокатні методи можуть.
— Джон Скіт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.