Найкращий спосіб поєднати два або більше байтових масивів у 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.