Como converter um objeto em uma matriz de bytes em C #


99

Tenho uma coleção de objetos que preciso gravar em um arquivo binário.

Preciso que os bytes do arquivo sejam compactos, então não posso usar BinaryFormatter. BinaryFormatterlança todos os tipos de informações para necessidades de desserialização.

Se eu tentar

byte[] myBytes = (byte[]) myObject 

Recebo uma exceção de tempo de execução.

Preciso que isso seja rápido, então prefiro não copiar matrizes de bytes. Só gostaria que o elenco byte[] myBytes = (byte[]) myObjectfuncionasse!

OK apenas para ser claro, eu não posso ter qualquer metadados no arquivo de saída. Apenas os bytes do objeto. Objeto a objeto empacotado. Com base nas respostas recebidas, parece que estarei escrevendo um Buffer.BlockCopycódigo de baixo nível . Talvez usando código inseguro.

Respostas:


173

Para converter um objeto em uma 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();
    }
}

Você só precisa copiar esta função para o seu código e enviar para ela o objeto que você precisa converter em uma matriz de bytes. Se precisar converter a matriz de bytes em um objeto novamente, você pode usar a função abaixo:

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

Você pode usar essas funções com classes personalizadas. Você só precisa adicionar o [Serializable]atributo em sua classe para habilitar a serialização


9
Eu tentei isso e adicionou todos os tipos de metadados. O OP disse que não queria metadados.
user316117

4
Sem mencionar que todos parecem presumir que o que você está tentando serializar é algo que você escreveu ou já foi pré-configurado para ser serializado.
Hexum064 de

3
Você pode passar a matriz de bytes diretamente para o construtor de MemoryStreamno segundo exemplo de código. Isso eliminaria o uso de Write(...)e Seek(...).
desconhecido6656

41

Se quiser que os dados serializados sejam realmente compactos, você mesmo pode escrever métodos de serialização. Dessa forma, você terá um mínimo de sobrecarga.

Exemplo:

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

}

o que é que eu tenho vários ints para escrever e várias strings?
Smith

1
@Smith: Sim, você pode fazer isso, basta escrevê-los um após o outro. Eles BinaryWriteros escreverão em um formato que BinaryReaderpossa ler, contanto que você os escreva e leia na mesma ordem.
Guffa

1
qual é a diferença entre BinaryWriter/Readere usando umBinaryFormatter
Smith

3
@Smith: Usando BinaryWriter/Readervocê mesmo, você faz a serialização / desserialização e pode escrever / ler apenas os dados absolutamente necessários, da forma mais compacta possível. O BinaryFormatterusa reflexão para descobrir quais dados gravar / ler e usa um formato que funciona para todos os casos possíveis. Ele também inclui as metainformações sobre o formato no fluxo, de modo que adiciona ainda mais sobrecarga.
Guffa

1
@Smith: você pode converter o enum para int(ou se tiver especificado qualquer outro tipo como armazenamento para o enum) e gravá-lo. Ao lê-lo, você pode convertê-lo no tipo enum.
Guffa

31

Bem, um elenco de myObjectpara byte[]nunca vai funcionar, a menos que você tenha uma conversão explícita ou se myObject for um byte[]. Você precisa de algum tipo de estrutura de serialização . Existem muitos por aí, incluindo Protocol Buffers que são meus queridos. É bastante "enxuto e mesquinho" em termos de espaço e tempo.

Você descobrirá que quase todas as estruturas de serialização têm restrições significativas sobre o que você pode serializar - Buffers de protocolo mais do que alguns, por serem multiplataforma.

Se você puder fornecer mais requisitos, podemos ajudá-lo mais - mas nunca será tão simples quanto lançar ...

EDITAR: Apenas para responder a isto:

Preciso que meu arquivo binário contenha os bytes do objeto. Apenas os bytes, nenhum metadado. Objeto a objeto empacotado. Portanto, implementarei a serialização personalizada.

Por favor, tenha em mente que os bytes em seus objetos são frequentemente referências ... então você precisará descobrir o que fazer com eles.

Suspeito que você descobrirá que projetar e implementar sua própria estrutura de serialização personalizada é mais difícil do que você imagina.

Eu pessoalmente recomendaria que, se você só precisar fazer isso para alguns tipos específicos, não se preocupe em tentar criar uma estrutura geral de serialização. Basta implementar um método de instância e um método estático em todos os tipos de que você precisa:

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

Uma coisa a ter em mente: tudo se torna mais complicado se você tiver herança envolvida. Sem herança, se você sabe com que tipo está começando, não precisa incluir nenhuma informação de tipo. Claro, há também a questão do controle de versão - você precisa se preocupar com a compatibilidade com versões anteriores e posteriores com diferentes versões de seus tipos?


É mais correto referir-me a isso como "protobuf-csharp-port" (código do Google) ou "dotnet-protobufs" (Git)?
Marc Gravell

1
Preciso que meu arquivo binário contenha os bytes do objeto. Apenas os bytes, nenhum metadado. Objeto a objeto empacotado. Portanto, implementarei a serialização personalizada.
chuckhlogan

6
O risco de zero metadados é que você fica muito intolerante a versões, pois há muito poucas maneiras de permitir flexibilidade antes que seja tarde demais. Os buffers de protocolo são bastante densos em dados. Você realmente precisa daquela volta extra do parafuso?
Marc Gravell

@Marc: E, claro, para inteiros, PB pode acabar sendo mais denso que os bytes brutos ...
Jon Skeet

16

Eu peguei a resposta da Crystalonics e as transformei em métodos de extensão. Espero que outra pessoa os considere úteis:

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 é realmente útil e fácil !! Obrigado.
MrHIDEn

13

Você está realmente falando sobre serialização, que pode assumir muitas formas. Já que você quer pequenos e binários, os buffers de protocolo podem ser uma opção viável - oferecendo tolerância de versão e portabilidade também. Ao contrário BinaryFormatter, o formato de fio dos buffers de protocolo não inclui todos os metadados do tipo; apenas marcadores muito concisos para identificar dados.

No .NET, existem algumas implementações; em particular

Eu humildemente argumentaria que protobuf-net (que escrevi) permite mais uso idiomático do .NET com classes C # típicas (buffers de protocolo "regulares" tendem a exigir geração de código); por exemplo:

[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
Mesmo "marcadores concisos" ainda são metadados. Minha compreensão do que o OP queria nada mais era do que os dados do objeto. Portanto, por exemplo, se o objeto fosse uma estrutura com 2 inteiros de 32 bits, ele esperaria que o resultado fosse uma matriz de bytes de 8 bytes.
user316117

@ user316117 que é então uma verdadeira dor para o controle de versão. Cada abordagem tem vantagens e desvantagens.
Marc Gravell


Existe uma maneira de evitar o uso dos atributos Proto *? As entidades que desejo usar estão em uma biblioteca de terceiros.
Alex 75 de

5

Isso funcionou para mim:

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

foo é um objeto do qual estou 100% certo que é uma matriz de bytes.


2

Dê uma olhada em Serialização , uma técnica para "converter" um objeto inteiro em um fluxo de bytes. Você pode enviá-lo para a rede ou gravá-lo em um arquivo e restaurá-lo posteriormente em um objeto.


Acho que chuckhlogan recusou explicitamente isso (Formatador == Serialização).
Henk Holterman

@Henk - depende de quais são os motivos ; ele mencionou as informações extras, que considero serem metadados de tipo e informações de campo; você pode usar a serialização sem essa sobrecarga; apenas não com BinaryFormatter.
Marc Gravell

2

Encontrei outra maneira de converter um objeto em um byte [], aqui está minha solução:

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

Saudações


1

Para acessar a memória de um objeto diretamente (para fazer um "core dump"), você precisará acessar o código não seguro.

Se você quiser algo mais compacto do que BinaryWriter ou um despejo de memória bruto fornecerá, você precisa escrever algum código de serialização personalizado que extraia as informações críticas do objeto e as empacote de maneira ideal.

editar PS É muito fácil envolver a abordagem BinaryWriter em um DeflateStream para compactar os dados, o que geralmente reduzirá aproximadamente pela metade o tamanho dos dados.


1
Código inseguro não é suficiente. C # e CLR ainda não permitem que você pegue um ponteiro bruto para um objeto gerenciado, mesmo em código não seguro, ou coloque duas referências de objeto em uma união.
Pavel Minaev

0

Eu acredito que o que você está tentando fazer é impossível.

O lixo BinaryFormattercriado é necessário para recuperar o objeto do arquivo depois que o programa for interrompido.
No entanto, é possível obter os dados do objeto, você só precisa saber o tamanho exato deles (mais difícil do 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;
}

isso pode ser recuperado via:

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

A razão pela qual os métodos acima não funcionam para serialização é que os primeiros quatro bytes nos dados retornados correspondem a a RuntimeTypeHandle. O RuntimeTypeHandledescreve o layout / tipo do objeto, mas o valor dele muda toda vez que o programa é executado.

EDIT: que estúpido não faça isso -> Se você já sabe o tipo do objeto a ser desserializado com certeza você pode trocar esses bytes BitConvertes.GetBytes((int)typeof(yourtype).TypeHandle.Value)no momento da desserialização.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.