Jak przekonwertować obiekt na tablicę bajtów w języku C #


102

Mam zbiór obiektów, które muszę zapisać w pliku binarnym.

Potrzebuję, aby bajty w pliku były kompaktowe, więc nie mogę ich użyć BinaryFormatter. BinaryFormatterzgłasza wszelkiego rodzaju informacje na potrzeby deserializacji.

Jeśli spróbuję

byte[] myBytes = (byte[]) myObject 

Otrzymuję wyjątek czasu wykonywania.

Potrzebuję tego, aby był szybki, więc wolałbym nie kopiować tablic bajtów. Chciałbym tylko, żeby obsada byte[] myBytes = (byte[]) myObjectdziałała!

OK, żeby było jasne, nie mogę mieć żadnych metadanych w pliku wyjściowym. Tylko bajty obiektu. Upakowany obiekt-obiekt. Na podstawie otrzymanych odpowiedzi wygląda na to, że będę pisać Buffer.BlockCopykod niskiego poziomu . Być może przy użyciu niebezpiecznego kodu.

Odpowiedzi:


180

Aby przekonwertować obiekt na tablicę bajtów:

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

Wystarczy skopiować tę funkcję do swojego kodu i wysłać do niego obiekt, który chcesz przekonwertować na tablicę bajtów. Jeśli potrzebujesz ponownie przekonwertować tablicę bajtów na obiekt, możesz użyć poniższej funkcji:

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

Możesz używać tych funkcji z niestandardowymi klasami. Wystarczy dodać [Serializable]atrybut w swojej klasie, aby włączyć serializację


9
Spróbowałem tego i dodałem różnego rodzaju metadane. OP powiedział, że nie chce metadanych.
user316117

4
Nie wspominając o tym, że wszyscy wydają się zakładać, że to, co próbujesz serializować, jest czymś, co napisałeś lub zostało już wstępnie skonfigurowane do serializacji.
Hexum064

3
Tablicę bajtów można przekazać bezpośrednio do konstruktora MemoryStreamw drugim przykładzie kodu. To wyeliminowałoby użycie Write(...)i Seek(...).
unknown6656

41

Jeśli chcesz, aby serializowane dane były naprawdę kompaktowe, możesz samodzielnie napisać metody serializacji. W ten sposób będziesz mieć minimum kosztów ogólnych.

Przykład:

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

}

co mam kilka int do napisania i kilka stringów?
Smith

1
@Smith: Tak, możesz to zrobić, po prostu napisz je po sobie. BinaryWriterZapisze je w formacie, który BinaryReadermożna odczytać, jak długo, jak pisać i czytać je w tej samej kolejności.
Guffa

1
jaka jest różnica między BinaryWriter/Readera używaniemBinaryFormatter
Smith

3
@Smith: Korzystając BinaryWriter/Readerz samodzielnego wykonania serializacji / deserializacji, możesz zapisywać / odczytywać tylko te dane, które są absolutnie potrzebne, tak zwarte, jak to tylko możliwe. BinaryFormatterOdbicie zastosowania dowiedzieć się, jakie dane do zapisu / odczytu i wykorzystuje format, który działa dla wszystkich możliwych przypadków. Zawiera również metainformacje o formacie w strumieniu, co powoduje jeszcze większe obciążenie.
Guffa

1
@Smith: Możesz rzutować wyliczenie na int(lub jeśli określiłeś inny typ jako magazyn dla wyliczenia) i zapisać go. Kiedy to przeczytasz, możesz rzucić to na typ wyliczenia.
Guffa

31

Dobrze odlew z myObjectaby byte[]nigdy nie będzie działać, chyba że masz wyraźne konwersję lub jeśli myObject jestbyte[] . Musisz ram serializacji jakiegoś rodzaju. Jest ich wiele, w tym bufory protokołów, które są mi bliskie i drogie. Jest dość „chudy i wredny” zarówno pod względem przestrzeni, jak i czasu.

Przekonasz się, że prawie wszystkie struktury serializacji mają znaczące ograniczenia dotyczące tego, co można serializować - więcej niż niektóre bufory protokołów, ponieważ są wieloplatformowe.

Jeśli możesz podać więcej wymagań, możemy Ci pomóc - ale nigdy nie będzie to tak proste, jak rzucanie ...

EDYCJA: Wystarczy odpowiedzieć na to:

Potrzebuję, aby mój plik binarny zawierał bajty obiektu. Tylko bajty, żadnych metadanych. Upakowany obiekt-obiekt. Więc zaimplementuję niestandardową serializację.

Pamiętaj, że bajty w twoich obiektach są dość często odwołaniami ... więc musisz dowiedzieć się, co z nimi zrobić.

Podejrzewam, że projektowanie i wdrażanie własnej niestandardowej platformy serializacji jest trudniejsze niż sobie wyobrażasz.

Osobiście poleciłbym, jeśli potrzebujesz to zrobić tylko dla kilku określonych typów, nie zawracaj sobie głowy próbą wymyślenia ogólnej struktury serializacji. Po prostu zaimplementuj metodę instancji i metodę statyczną we wszystkich potrzebnych typach:

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

Należy pamiętać o jednej rzeczy: wszystko staje się trudniejsze, jeśli w grę wchodzi dziedziczenie. Bez dziedziczenia, jeśli wiesz, od jakiego typu zaczynasz, nie musisz uwzględniać żadnych informacji o typie. Oczywiście jest też kwestia wersjonowania - czy musisz się martwić o wsteczną i przyszłą kompatybilność z różnymi wersjami twoich typów?


Czy lepiej jest nazywać to „protobuf-csharp-port” (kod Google) czy „dotnet-protobufs” (Git)?
Marc Gravell

1
Potrzebuję, aby mój plik binarny zawierał bajty obiektu. Tylko bajty, żadnych metadanych. Upakowany obiekt-obiekt. Więc zaimplementuję niestandardową serializację.
chuckhlogan

6
Ryzyko braku metadanych polega na tym, że jesteś wtedy bardzo nietolerancyjny dla wersji, ponieważ ma bardzo niewiele sposobów na zapewnienie elastyczności, zanim będzie za późno. Bufory protokołów zawierają dużo danych. Czy naprawdę potrzebujesz tego dodatkowego obrotu śruby?
Marc Gravell

@Marc: I oczywiście w przypadku liczb całkowitych PB może być gęstsze niż surowe bajty ...
Jon Skeet

16

Wziąłem odpowiedź Crystalonics i przekształciłem je w metody rozszerzające. Mam nadzieję, że ktoś inny uzna je za przydatne:

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
Ten jest naprawdę przydatny i łatwy! Dziękuję Ci.
MrHIDEn

13

Naprawdę mówisz o serializacji, która może przybierać różne formy. Ponieważ chcesz małych i binarnych, bufory protokołów mogą być realną opcją - zapewniającą również tolerancję wersji i przenośność. W przeciwieństwie do tego BinaryFormatter, format łącznika buforów protokołów nie obejmuje wszystkich metadanych typu; po prostu bardzo zwięzłe znaczniki do identyfikacji danych.

W .NET istnieje kilka implementacji; w szczególności

Pokornie twierdzę, że protobuf-net (który napisałem) pozwala na większe użycie idiomatycznego .NET z typowymi klasami C # („zwykłe” bufory protokołów zwykle wymagają generowania kodu); na przykład:

[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
Nawet „zwięzłe znaczniki” są nadal metadanymi. Moje zrozumienie tego, czego chciał OP, było niczym innym, jak danymi w obiekcie. Na przykład, jeśli obiekt byłby strukturą z 2 32-bitowymi liczbami całkowitymi, to spodziewałby się, że wynikiem będzie tablica bajtów o długości 8 bajtów.
user316117

@ user316117, co jest wtedy prawdziwym problemem dla wersjonowania. Każde podejście ma zalety i wady.
Marc Gravell


Czy jest sposób, aby uniknąć używania atrybutów Proto *? Elementy, których chcę użyć, znajdują się w bibliotece innej firmy.
Alex 75

5

To zadziałało dla mnie:

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

foo jest obiektem, co do którego mam 100% pewności, że jest tablicą bajtów.


2

Przyjrzyj się serializacji , technice „konwertowania” całego obiektu na strumień bajtów. Możesz wysłać go do sieci lub zapisać do pliku, a następnie przywrócić później do obiektu.


Myślę, że Chuckhlogan wyraźnie to odrzucił (Formatter == Serializacja).
Henk Holterman

@Henk - to zależy, jakie są przyczyny ; wspomniał o dodatkowych informacjach, które uważam za typ metadanych i informacji o polu; możesz używać serializacji bez tego narzutu; po prostu nie z BinaryFormatter.
Marc Gravell

2

Znalazłem inny sposób konwersji obiektu na bajt [], oto moje rozwiązanie:

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

pozdrowienia


1

Aby uzyskać bezpośredni dostęp do pamięci obiektu (aby wykonać „zrzut pamięci”), musisz przejść do niebezpiecznego kodu.

Jeśli chcesz czegoś bardziej kompaktowego niż BinaryWriter lub surowy zrzut pamięci, musisz napisać niestandardowy kod serializacji, który wyodrębnia krytyczne informacje z obiektu i pakuje je w optymalny sposób.

edit PS Bardzo łatwo jest zawinąć podejście BinaryWriter do DeflateStream w celu skompresowania danych, co zwykle zmniejsza rozmiar danych o około połowę.


1
Niebezpieczny kod nie wystarczy. C # i środowisko CLR nadal nie pozwalają na pobranie surowego wskaźnika do obiektu zarządzanego nawet w niebezpiecznym kodzie ani umieszczenie dwóch odwołań do obiektów w unii.
Pavel Minaev

1

Wierzę, że to, co próbujesz zrobić, jest niemożliwe.

Tworzone śmieci BinaryFormattersą niezbędne do odzyskania obiektu z pliku po zatrzymaniu programu.
Jednak możliwe jest uzyskanie danych obiektu, wystarczy znać dokładny ich rozmiar (trudniejszy niż się wydaje):

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

można to odzyskać poprzez:

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

Powodem, dla którego powyższe metody nie działają w przypadku serializacji, jest to, że pierwsze cztery bajty w zwróconych danych odpowiadają rozszerzeniu RuntimeTypeHandle. RuntimeTypeHandleOpisuje Układ / typ obiektu, ale jego wartość zmienia się za każdym razem, gdy program jest prowadzony.

EDYCJA: to jest głupie, nie rób tego -> Jeśli znasz już typ obiektu do deserializacji na pewno, możesz zamienić te bajty BitConvertes.GetBytes((int)typeof(yourtype).TypeHandle.Value)w momencie deserializacji.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.