Cómo convertir un objeto en una matriz de bytes en C #


101

Tengo una colección de objetos que necesito escribir en un archivo binario.

Necesito que los bytes del archivo sean compactos, así que no puedo usarlos BinaryFormatter. BinaryFormatterarroja todo tipo de información para las necesidades de deserialización.

Si lo intento

byte[] myBytes = (byte[]) myObject 

Recibo una excepción de tiempo de ejecución.

Necesito que esto sea rápido, así que prefiero no copiar matrices de bytes. ¡Me gustaría que el elenco byte[] myBytes = (byte[]) myObjectfuncionara!

Aceptar sólo para que quede claro, no puedo tener ninguna metadatos en el archivo de salida. Solo los bytes del objeto. Empaquetado de objeto a objeto. Según las respuestas recibidas, parece que estaré escribiendo Buffer.BlockCopycódigo de bajo nivel . Quizás usando código inseguro.

Respuestas:


176

Para convertir un objeto en una matriz de bytes:

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

Solo necesita copiar esta función en su código y enviarle el objeto que necesita convertir en una matriz de bytes. Si necesita convertir la matriz de bytes en un objeto nuevamente, puede usar la siguiente función:

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

Puede utilizar estas funciones con clases personalizadas. Solo necesita agregar el [Serializable]atributo en su clase para habilitar la serialización


9
Probé esto y agregó todo tipo de metadatos. El OP dijo que no quería metadatos.
user316117

4
Sin mencionar que todo el mundo parece suponer que lo que está intentando serializar es algo que ha escrito o que ya ha sido preconfigurado para ser serializado.
Hexum064

3
Puede pasar la matriz de bytes directamente al constructor de MemoryStreamen el segundo ejemplo de código. Esto eliminaría el uso de Write(...)y Seek(...).
unknown6656

41

Si desea que los datos serializados sean realmente compactos, puede escribir métodos de serialización usted mismo. De esa forma tendrá un mínimo de gastos generales.

Ejemplo:

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

}

¿Qué tengo varias entradas para escribir y varias cadenas?
Smith

1
@Smith: Sí, puedes hacerlo, solo escríbelos uno después del otro. Los BinaryWriterescribirá en un formato que BinaryReaderpueda leer, siempre que los escriba y lea en el mismo orden.
Guffa

1
¿Cuál es la diferencia entre BinaryWriter/Readery usar un?BinaryFormatter
Smith

3
@Smith: Utilizando BinaryWriter/Readerusted mismo hace la serialización / deserialización, y puede escribir / leer solo los datos que son absolutamente necesarios, lo más compacto posible. La BinaryFormatterreflexión usos para averiguar qué datos de escritura / lectura, y utiliza un formato que funcione para todos los casos posibles. También incluye la metainformación sobre el formato en la transmisión, por lo que agrega aún más gastos generales.
Guffa

1
@Smith: puede enviar la enumeración a int(o si ha especificado cualquier otro tipo como almacenamiento para la enumeración) y escribirla. Cuando lo lea, puede convertirlo en el tipo enum.
Guffa

31

Bueno, un elenco de myObjecta byte[]nunca funcionará a menos que tengas una conversión explícita o si myObject es un byte[]. Necesita un marco de serialización de algún tipo. Hay muchos por ahí, incluidos los búferes de protocolo que son cercanos y queridos para mí. Es bastante "delgado y mezquino" en términos de espacio y tiempo.

Sin embargo, encontrará que casi todos los marcos de serialización tienen restricciones significativas sobre lo que puede serializar: búferes de protocolo más que algunos, debido a que son multiplataforma.

Si puede dar más requisitos, podemos ayudarlo más, pero nunca será tan simple como el casting ...

EDITAR: Solo para responder a esto:

Necesito que mi archivo binario contenga los bytes del objeto. Solo los bytes, sin metadatos de ningún tipo. Empaquetado de objeto a objeto. Así que implementaré la serialización personalizada.

Tenga en cuenta que los bytes de sus objetos suelen ser referencias ... por lo que deberá averiguar qué hacer con ellos.

Sospecho que encontrará que diseñar e implementar su propio marco de serialización personalizado es más difícil de lo que imagina.

Personalmente, recomendaría que si solo necesita hacer esto para algunos tipos específicos, no se moleste en intentar crear un marco de serialización general. Simplemente implemente un método de instancia y un método estático en todos los tipos que necesite:

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

Una cosa a tener en cuenta: todo se vuelve más complicado si se trata de una herencia. Sin herencia, si sabe con qué tipo está comenzando, no necesita incluir ningún tipo de información. Por supuesto, también está la cuestión del control de versiones: ¿necesita preocuparse por la compatibilidad hacia atrás y hacia adelante con diferentes versiones de sus tipos?


¿Es más correcto para mí referirme a esto como "protobuf-csharp-port" (código de Google) o "dotnet-protobufs" (Git)?
Marc Gravell

1
Necesito que mi archivo binario contenga los bytes del objeto. Solo los bytes, sin metadatos de ningún tipo. Empaquetado de objeto a objeto. Así que implementaré la serialización personalizada.
chuckhlogan

6
El riesgo de cero metadatos es que entonces eres muy intolerante a la versión, ya que tiene muy pocas formas de permitir la flexibilidad antes de que sea demasiado tarde. Los búferes de protocolo son bastante densos en datos. ¿Realmente necesitas ese giro extra del tornillo?
Marc Gravell

@Marc: Y, por supuesto, para los enteros, PB puede terminar siendo más denso que los bytes sin procesar ...
Jon Skeet

16

Tomé la respuesta de Crystalonics y los convertí en métodos de extensión. Espero que alguien más los encuentre útiles:

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
¡Este es realmente útil y fácil! Gracias.
MrHIDEn

13

Realmente estás hablando de serialización, que puede tomar muchas formas. Dado que desea pequeños y binarios, los búferes de protocolo pueden ser una opción viable, ya que también brindan tolerancia y portabilidad de versiones. A diferencia BinaryFormatter, el formato de cable de los búferes de protocolo no incluye todos los metadatos de tipo; simplemente marcadores muy concisos para identificar datos.

En .NET hay algunas implementaciones; en particular

Yo diría humildemente que protobuf-net (que escribí) permite un uso más idiomático de .NET con clases típicas de C # (los búferes de protocolo "regulares" tienden a exigir la generación de código); por ejemplo:

[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
Incluso los "marcadores concisos" siguen siendo metadatos. Mi comprensión de lo que quería el OP no era más que los datos en el objeto. Entonces, por ejemplo, si el objeto fuera una estructura con 2 enteros de 32 bits, entonces esperaría que el resultado fuera una matriz de bytes de 8 bytes.
user316117

@ user316117, que es un verdadero problema para el control de versiones. Cada enfoque tiene ventajas y desventajas.
Marc Gravell


¿Hay alguna forma de evitar el uso de los atributos Proto *? Las entidades que quiero usar están en una biblioteca de terceros.
Alex 75

5

Esto funcionó para mí:

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

foo es un objeto del que estoy 100% seguro de que es una matriz de bytes.


2

Eche un vistazo a la serialización , una técnica para "convertir" un objeto completo en un flujo de bytes. Puede enviarlo a la red o escribirlo en un archivo y luego restaurarlo a un objeto más tarde.


Creo que chuckhlogan rechazó explícitamente eso (Formateador == Serialización).
Henk Holterman

@Henk: depende de las razones ; mencionó la información adicional, que considero que son metadatos de tipo e información de campo; puede usar la serialización sin esa sobrecarga; simplemente no con BinaryFormatter.
Marc Gravell

2

Encontré otra forma de convertir un objeto en un byte [], aquí está mi solución:

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

Saludos


1

Para acceder a la memoria de un objeto directamente (para hacer un "volcado de memoria"), deberá dirigirse al código inseguro.

Si desea algo más compacto que BinaryWriter o un volcado de memoria sin procesar, debe escribir un código de serialización personalizado que extraiga la información crítica del objeto y la empaquete de una manera óptima.

editar PS Es muy fácil envolver el enfoque BinaryWriter en un DeflateStream para comprimir los datos, lo que generalmente reducirá aproximadamente a la mitad el tamaño de los datos.


1
El código inseguro no es suficiente. C # y CLR aún no le permitirán tomar un puntero sin procesar a un objeto administrado, incluso en código inseguro, o colocar dos referencias de objeto en una unión.
Pavel Minaev

1

Creo que lo que intentas hacer es imposible.

La basura que BinaryFormattercrea es necesaria para recuperar el objeto del archivo después de que se detuvo el programa.
Sin embargo, es posible obtener los datos del objeto, solo necesita saber el tamaño exacto (más difícil de lo que parece):

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

esto se puede recuperar a través de:

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

La razón por la que los métodos anteriores no funcionan para la serialización es que los primeros cuatro bytes de los datos devueltos corresponden a un RuntimeTypeHandle. La RuntimeTypeHandledescribe el diseño / tipo del objeto, pero el valor de la misma cambia cada vez que el programa fue ejecutado.

EDITAR: eso es estúpido, no hagas eso -> Si ya sabes con certeza el tipo de objeto que se va a deserializar, puedes cambiar esos bytes BitConvertes.GetBytes((int)typeof(yourtype).TypeHandle.Value)en el momento de la deserialización.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.