Як перетворити об'єкт на байтовий масив у C #


101

У мене є колекція об’єктів, які мені потрібно записати у двійковий файл.

Мені потрібні байти у файлі, щоб бути компактними, тому я не можу використовувати BinaryFormatter. BinaryFormatterкидає всіляку інформацію для потреб десериалізації.

Якщо я спробую

byte[] myBytes = (byte[]) myObject 

Я отримую виняток виконання.

Мені потрібно, щоб це було швидко, тому я б не хотів копіювати масиви байтів навколо. Я б просто хотів, щоб акторський склад byte[] myBytes = (byte[]) myObjectпрацював!

Добре, щоб було зрозуміло, я не можу мати жодних метаданих у вихідному файлі. Просто байти об’єкта. Упакований об’єкт-об’єкт. На основі отриманих відповідей, схоже, я буду писати низькорівневий Buffer.BlockCopyкод. Можливо, використовуючи небезпечний код.

Відповіді:


176

Щоб перетворити об’єкт у байтовий масив:

// Convert an object to a byte array
public static byte[] ObjectToByteArray(Object obj)
{
    BinaryFormatter bf = new BinaryFormatter();
    using (var ms = new MemoryStream())
    {
        bf.Serialize(ms, obj);
        return ms.ToArray();
    }
}

Вам просто потрібно скопіювати цю функцію у свій код і надіслати їй об’єкт, який вам потрібно перетворити в байтовий масив. Якщо вам потрібно знову перетворити байтовий масив в об'єкт, ви можете скористатися наведеною нижче функцією:

// Convert a byte array to an Object
public static Object ByteArrayToObject(byte[] arrBytes)
{
    using (var memStream = new MemoryStream())
    {
        var binForm = new BinaryFormatter();
        memStream.Write(arrBytes, 0, arrBytes.Length);
        memStream.Seek(0, SeekOrigin.Begin);
        var obj = binForm.Deserialize(memStream);
        return obj;
    }
}

Ви можете використовувати ці функції зі спеціальними класами. Вам просто потрібно додати [Serializable]атрибут у своєму класі, щоб увімкнути серіалізацію


9
Я спробував це, і воно додало всілякі метадані. ОП заявив, що не хоче метаданих.
user316117

4
Не кажучи вже про те, що всі, здається, припускають, що те, що ви намагаєтесь серіалізувати, - це те, що ви написали або вже було попередньо налаштовано на серіалізацію.
Hexum064,

3
Ви можете передати байтовий масив безпосередньо конструктору MemoryStreamв другому прикладі коду. Це виключить використання Write(...)та Seek(...).
невідомо6656

41

Якщо ви хочете, щоб серіалізовані дані були дійсно компактними, ви можете написати методи серіалізації самостійно. Таким чином у вас буде мінімум накладних витрат.

Приклад:

public class MyClass {

   public int Id { get; set; }
   public string Name { get; set; }

   public byte[] Serialize() {
      using (MemoryStream m = new MemoryStream()) {
         using (BinaryWriter writer = new BinaryWriter(m)) {
            writer.Write(Id);
            writer.Write(Name);
         }
         return m.ToArray();
      }
   }

   public static MyClass Desserialize(byte[] data) {
      MyClass result = new MyClass();
      using (MemoryStream m = new MemoryStream(data)) {
         using (BinaryReader reader = new BinaryReader(m)) {
            result.Id = reader.ReadInt32();
            result.Name = reader.ReadString();
         }
      }
      return result;
   }

}

що в мене є кілька вводів для написання і кілька рядків?
Сміт

1
@Smith: Так, ти можеш це зробити, просто пиши їх один за одним. The BinaryWriterнапише їх у форматі, який BinaryReaderвміє читати, якщо ви пишете та читаєте їх у тому самому порядку.
Гуффа

1
яка різниця між BinaryWriter/Readerвикористанням та використаннямBinaryFormatter
Сміт

3
@Smith: Використовуючи BinaryWriter/Readerсеріалізацію / десериалізацію, ви можете писати / читати лише ті дані, які вкрай необхідні, якомога компактніші. BinaryFormatterВикористовує відображення , щоб з'ясувати , які дані для запису / зчитування, і використовує формат , який працює для всіх можливих випадків. Він також включає метаінформацію про формат у потоці, що додає ще більше накладних витрат.
Гуффа

1
@Smith: Ви можете передати перерахування int(або якщо ви вказали будь-який інший тип як сховище для переліку) і записати його. Коли ви читаєте його, ви можете передати його до типу переліку.
Гуффа

31

Ну, амплітуда з myObjectдо byte[]ніколи не буде працювати, якщо у вас немає явного перетворення або якщо myObject це а byte[]. Вам потрібна сериализация рамка якого - то. Там є багато, включаючи буфери протоколів, який мені близький і дорогий. Це досить «худий і підлий» як з точки зору простору, так і часу.

Ви виявите, що майже всі фреймворки серіалізації мають суттєві обмеження щодо того, що можна серіалізувати, проте - буфери протоколів більше, ніж деякі, через те, що вони є міжплатформними.

Якщо ви можете надати більше вимог, ми можемо вам допомогти більше - але це ніколи не буде так просто, як кастинг ...

EDIT: Просто щоб відповісти на це:

Мені потрібен мій двійковий файл, щоб містити байти об’єкта. Тільки байти, ніяких метаданих взагалі. Упакований об’єкт-об’єкт. Тож я буду впроваджувати спеціальну серіалізацію.

Будь ласка, майте на увазі, що байти у ваших об’єктах досить часто є посиланнями ... тому вам потрібно буде з’ясувати, що з ними робити.

Підозрюю, ви виявите, що розробляти та впроваджувати власну структуру серіалізації складніше, ніж ви собі уявляєте.

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

public void WriteTo(Stream stream)
public static WhateverType ReadFrom(Stream stream)

Майте на увазі одне: все стає складнішим, якщо у вас є спадщина. Без успадкування, якщо ви знаєте, з якого типу починаєте, вам не потрібно включати інформацію про тип. Звичайно, тут також питання версій - чи потрібно турбуватися про зворотну та пряму сумісність з різними версіями ваших типів?


Чи правильніше для мене це називати "protobuf-csharp-port" (Google-код) або "dotnet-protobufs" (Git)?
Марк Гравелл

1
Мені потрібен мій двійковий файл, щоб містити байти об’єкта. Тільки байти, ніяких метаданих взагалі. Упакований об’єкт-об’єкт. Тож я буду впроваджувати спеціальну серіалізацію.
chuckhlogan

6
Ризик нульових метаданих є те , що ви тоді дуже версією непереносимості, так як вона має дуже мало способів забезпечення гнучкості , поки ще не занадто пізно. Буфери протоколів досить щільні. Вам справді потрібен той додатковий поворот гвинта?
Марк Гравелл

@Marc: І, звичайно, для цілих чисел PB може виявитись щільнішим за необроблені байти ...
Джон Скіт,

16

Я взяв відповідь Crystalonics і перетворив їх на методи розширення. Сподіваюсь, комусь іншому вони знадобляться:

public static byte[] SerializeToByteArray(this object obj)
{
    if (obj == null)
    {
        return null;
    }
    var bf = new BinaryFormatter();
    using (var ms = new MemoryStream())
    {
        bf.Serialize(ms, obj);
        return ms.ToArray();
    }
}

public static T Deserialize<T>(this byte[] byteArray) where T : class
{
    if (byteArray == null)
    {
        return null;
    }
    using (var memStream = new MemoryStream())
    {
        var binForm = new BinaryFormatter();
        memStream.Write(byteArray, 0, byteArray.Length);
        memStream.Seek(0, SeekOrigin.Begin);
        var obj = (T)binForm.Deserialize(memStream);
        return obj;
    }
}

1
Це дійсно корисно і просто !! Дякую.
MrHIDEn

13

Ви справді говорите про серіалізацію, яка може приймати різні форми. Оскільки вам потрібні дрібні та двійкові файли, буфери протоколів можуть бути життєздатним варіантом, надаючи також толерантність до версії та портативність. На відміну від цього BinaryFormatter, формат дротових буферів протоколів не включає всі метадані типу; просто дуже стислі маркери для ідентифікації даних.

У .NET є кілька реалізацій; зокрема

Я смиренно стверджую, що protobuf-net (про що я писав) дозволяє більше .NET-ідіоматичного використання з типовими класами C # ("звичайні" буфери протоколів, як правило, вимагають генерації коду); наприклад:

[ProtoContract]
public class Person {
   [ProtoMember(1)]
   public int Id {get;set;}
   [ProtoMember(2)]
   public string Name {get;set;}
}
....
Person person = new Person { Id = 123, Name = "abc" };
Serializer.Serialize(destStream, person);
...
Person anotherPerson = Serializer.Deserialize<Person>(sourceStream);

1
Навіть "стислі маркери" все ще є метаданими. Я розумів, чого хоче ОП, - це не що інше, як дані в об’єкті. Так, наприклад, якби об’єкт був структурою з 2 32-розрядними цілими числами, то він би очікував, що результатом буде байтовий масив із 8 байт.
user316117

@ user316117, що тоді є справжнім болем для версій. Кожен підхід має переваги та недоліки.
Марк Гравелл


Є спосіб уникнути використання атрибутів Proto *? Сутності, якими я хочу скористатися, знаходяться в сторонній бібліотеці.
Alex 75,

5

Це працювало для мене:

byte[] bfoo = (byte[])foo;

foo - це об’єкт, у якому я на 100% впевнений, що це байтовий масив.


2

Погляньте на серіалізацію , техніку для "перетворення" цілого об'єкта в байтовий потік. Ви можете надіслати його в мережу або записати у файл, а потім відновити назад до об’єкта пізніше.


Я думаю, chuckhlogan явно відмовився (Formatter == Серіалізація).
Хенк Холтерман

@Henk - це залежить від причин ; він згадав додаткову інформацію, яку я вважаю метаданими типу та інформацією поля; ви можете використовувати серіалізацію без цих накладних витрат; просто не з BinaryFormatter.
Марк Гравелл

2

Я знайшов інший спосіб перетворити об'єкт у байт [], ось моє рішення:

IEnumerable en = (IEnumerable) myObject;
byte[] myBytes = en.OfType<byte>().ToArray();

З повагою


1

Щоб отримати прямий доступ до пам'яті об'єкта (щоб зробити "дамп ядра"), вам потрібно перейти в небезпечний код.

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

редагувати PS Дуже легко обернути підхід BinaryWriter у DeflateStream для стиснення даних, що, як правило, приблизно вдвічі зменшує розмір даних.


1
Небезпечного коду недостатньо. C # і CLR все одно не дозволяють вам взяти необроблений вказівник на керований об'єкт навіть у небезпечному коді або помістити два посилання на об'єкти в об'єднання.
Павло Мінаєв

1

Я вважаю, що те, що ви намагаєтеся зробити, неможливо.

Смітник, який BinaryFormatterстворюється, необхідний для відновлення об’єкта з файлу після зупинки програми.
Однак отримати дані об’єкта можливо, вам просто потрібно знати точний їх розмір (складніше, ніж це звучить):

public static unsafe byte[] Binarize(object obj, int size)
{
    var r = new byte[size];
    var rf = __makeref(obj);
    var a = **(IntPtr**)(&rf);
    Marshal.Copy(a, r, 0, size);
    return res;
}

це можна відновити за допомогою:

public unsafe static dynamic ToObject(byte[] bytes)
{
    var rf = __makeref(bytes);
    **(int**)(&rf) += 8;
    return GCHandle.Alloc(bytes).Target;
}

Причина, чому вищезазначені методи не працюють для серіалізації, полягає в тому, що перші чотири байти у повернутих даних відповідають a RuntimeTypeHandle. RuntimeTypeHandleОписує макет / тип об'єкта , але значення його змінюється кожен раз , коли програма побіг.

РЕДАГУВАТИ: це по-дурному, не робіть цього -> Якщо ви вже знаєте тип об’єкта, який потрібно десериалізувати, ви можете змінити ці байти BitConvertes.GetBytes((int)typeof(yourtype).TypeHandle.Value)на час десеріалізації.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.