Comment convertir un objet en tableau d'octets en C #


101

J'ai une collection d'objets dont j'ai besoin pour écrire dans un fichier binaire.

J'ai besoin que les octets du fichier soient compacts, donc je ne peux pas utiliser BinaryFormatter. BinaryFormatterjette toutes sortes d'informations pour les besoins de désérialisation.

Si j'essaye

byte[] myBytes = (byte[]) myObject 

J'obtiens une exception d'exécution.

J'ai besoin que cela soit rapide, donc je préfère ne pas copier des tableaux d'octets. J'aimerais juste que le casting byte[] myBytes = (byte[]) myObjectfonctionne!

OK pour être clair, je ne peux pas avoir de métadonnées dans le fichier de sortie. Juste les octets de l'objet. Objet à objet emballé. Sur la base des réponses reçues, il semble que j'écrirai du Buffer.BlockCopycode de bas niveau . Peut-être en utilisant un code non sécurisé.

Réponses:


176

Pour convertir un objet en tableau d'octets:

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

Il vous suffit de copier cette fonction dans votre code et de lui envoyer l'objet que vous devez convertir en un tableau d'octets. Si vous avez besoin de convertir à nouveau le tableau d'octets en objet, vous pouvez utiliser la fonction ci-dessous:

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

Vous pouvez utiliser ces fonctions avec des classes personnalisées. Il vous suffit d'ajouter l' [Serializable]attribut dans votre classe pour activer la sérialisation


9
J'ai essayé cela et cela a ajouté toutes sortes de métadonnées. Le PO a déclaré qu'il ne voulait pas de métadonnées.
user316117

4
Sans oublier que tout le monde semble supposer que ce que vous essayez de sérialiser est quelque chose que vous avez écrit ou qui a déjà été configuré pour être sérialisé.
Hexum064

3
Vous pouvez passer le tableau d'octets directement au constructeur de MemoryStreamdans le deuxième exemple de code. Cela éliminerait l'utilisation de Write(...)et Seek(...).
unknown6656

41

Si vous voulez que les données sérialisées soient vraiment compactes, vous pouvez écrire vous-même des méthodes de sérialisation. De cette façon, vous aurez un minimum de frais généraux.

Exemple:

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'est-ce que j'ai plusieurs entiers à écrire et plusieurs chaînes?
Smith

1
@Smith: Oui, vous pouvez le faire, écrivez-les simplement les uns après les autres. Le BinaryWriterles écrira dans un format que le BinaryReaderpeut lire, tant que vous les écrivez et lisez dans le même ordre.
Guffa

1
Quelle est la différence entre BinaryWriter/Readeret l'utilisation d'unBinaryFormatter
Smith

3
@Smith: En utilisant BinaryWriter/Readervous faites la sérialisation / désérialisation vous-même, et vous pouvez écrire / lire uniquement les données qui sont absolument nécessaires, aussi compactes que possible. Le BinaryFormatterutilise la réflexion pour savoir quelles données écrire / lire et utilise un format qui fonctionne dans tous les cas possibles. Il comprend également les méta-informations sur le format dans le flux, ce qui ajoute encore plus de surcharge.
Guffa

1
@Smith: Vous pouvez convertir l'énumération en int(ou si vous avez spécifié un autre type comme stockage pour l'énumération) et l'écrire. Lorsque vous le lisez, vous pouvez le convertir en type enum.
Guffa

31

Eh bien un casting de myObjectpour byte[]ne va jamais au travail à moins que vous avez une conversion explicite ou si myObject est un byte[]. Vous avez besoin d'un cadre de sérialisation quelconque . Il y en a beaucoup là-bas, y compris les tampons de protocole qui me sont chers. C'est assez «mince et méchant» en termes d'espace et de temps.

Vous constaterez que presque tous les frameworks de sérialisation ont des restrictions importantes sur ce que vous pouvez sérialiser, cependant - les tampons de protocole plus que certains, en raison du fait qu'ils sont multiplateformes.

Si vous pouvez donner plus d'exigences, nous pouvons vous aider davantage - mais ce ne sera jamais aussi simple que de lancer ...

EDIT: Juste pour répondre à ceci:

J'ai besoin de mon fichier binaire pour contenir les octets de l'objet. Seuls les octets, pas de métadonnées du tout. Objet à objet emballé. Je vais donc implémenter la sérialisation personnalisée.

Veuillez garder à l'esprit que les octets de vos objets sont assez souvent des références ... vous devrez donc déterminer quoi faire avec eux.

Je soupçonne que vous constaterez que la conception et la mise en œuvre de votre propre cadre de sérialisation personnalisé sont plus difficiles que vous ne l'imaginez.

Je recommanderais personnellement que si vous ne devez le faire que pour quelques types spécifiques, vous ne vous souciez pas d'essayer de créer un cadre de sérialisation général. Implémentez simplement une méthode d'instance et une méthode statique dans tous les types dont vous avez besoin:

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

Une chose à garder à l'esprit: tout devient plus délicat si l'héritage est impliqué. Sans héritage, si vous savez par quel type vous commencez, vous n'avez pas besoin d'inclure d'informations de type. Bien sûr, il y a aussi la question du versionnage - avez-vous besoin de vous soucier de la compatibilité ascendante et descendante avec différentes versions de vos types?


Est-il plus correct pour moi de faire référence à cela comme "protobuf-csharp-port" (code Google) ou "dotnet-protobufs" (Git)?
Marc Gravell

1
J'ai besoin de mon fichier binaire pour contenir les octets de l'objet. Seuls les octets, pas de métadonnées du tout. Objet à objet emballé. Je vais donc implémenter la sérialisation personnalisée.
chuckhlogan

6
Le risque de zéro métadonnée est que vous êtes alors très intolérant aux versions, car il a très peu de moyens de permettre la flexibilité avant qu'il ne soit trop tard. Les tampons de protocole sont assez riches en données. Avez-vous vraiment besoin de ce tour supplémentaire de vis?
Marc Gravell

@Marc: Et bien sûr pour les entiers, PB peut finir par être plus dense que les octets bruts ...
Jon Skeet

16

J'ai pris la réponse de Crystalonics et les ai transformées en méthodes d'extension. J'espère que quelqu'un d'autre les trouvera utiles:

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
Celui-ci est vraiment utile et facile !! Je vous remercie.
MrHIDEn

13

Vous parlez vraiment de sérialisation, qui peut prendre de nombreuses formes. Puisque vous voulez des petits et des binaires, les tampons de protocole peuvent être une option viable - offrant également une tolérance de version et une portabilité. À la différence BinaryFormatter, le format de fil des tampons de protocole n'inclut pas toutes les métadonnées de type; juste des marqueurs très laconiques pour identifier les données.

Dans .NET, il existe quelques implémentations; en particulier

Je soutiendrais humblement que protobuf-net (que j'ai écrit) permet une utilisation plus idiomatique de .NET avec des classes C # typiques (les tampons de protocole "normaux" ont tendance à exiger la génération de code); par exemple:

[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
Même les «marqueurs laconiques» sont toujours des métadonnées. Ma compréhension de ce que voulait l'OP n'était rien d'autre que les données de l'objet. Ainsi, par exemple, si l'objet était une structure avec 2 entiers de 32 bits, alors il s'attendrait à ce que le résultat soit un tableau d'octets de 8 octets.
user316117

@ user316117 ce qui est alors une vraie douleur pour le contrôle de version. Chaque approche présente des avantages et des inconvénients.
Marc Gravell


Existe-t-il un moyen d'éviter d'utiliser les attributs Proto *? Les entités que je souhaite utiliser se trouvent dans une bibliothèque tierce.
Alex 75

5

Cela a fonctionné pour moi:

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

foo est un objet dont je suis sûr à 100% qui est un tableau d'octets.


2

Jetez un œil à la sérialisation , une technique pour «convertir» un objet entier en un flux d'octets. Vous pouvez l'envoyer au réseau ou l'écrire dans un fichier, puis le restaurer ultérieurement dans un objet.


Je pense que chuckhlogan a explicitement refusé cela (Formatter == Serialization).
Henk Holterman

@Henk - cela dépend des raisons ; il a mentionné les informations supplémentaires, que je considère comme des métadonnées de type et des informations de champ; vous pouvez utiliser la sérialisation sans cette surcharge; mais pas avec BinaryFormatter.
Marc Gravell

2

J'ai trouvé un autre moyen de convertir un objet en octet [], voici ma solution:

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

Cordialement


1

Pour accéder directement à la mémoire d'un objet (pour faire un "core dump"), vous devrez vous diriger vers un code non sécurisé.

Si vous voulez quelque chose de plus compact que BinaryWriter ou qu'un vidage de la mémoire brute vous le donnera, vous devez écrire un code de sérialisation personnalisé qui extrait les informations critiques de l'objet et les emballe de manière optimale.

edit PS Il est très facile d'envelopper l'approche BinaryWriter dans un DeflateStream pour compresser les données, ce qui réduit généralement de moitié la taille des données.


1
Un code non sécurisé ne suffit pas. C # et CLR ne vous permettront toujours pas de prendre un pointeur brut vers un objet managé, même dans un code non sécurisé, ou de placer deux références d'objet dans une union.
Pavel Minaev

1

Je crois que ce que vous essayez de faire est impossible.

Le courrier indésirable qui BinaryFormattercrée est nécessaire pour récupérer l'objet à partir du fichier après l'arrêt de votre programme.
Cependant, il est possible d'obtenir les données de l'objet, il vous suffit de connaître la taille exacte de celui-ci (plus difficile qu'il n'y paraît):

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

cela peut être récupéré via:

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

La raison pour laquelle les méthodes ci-dessus ne fonctionnent pas pour la sérialisation est que les quatre premiers octets des données renvoyées correspondent à un RuntimeTypeHandle. Le RuntimeTypeHandledécrit la disposition / le type de l'objet mais la valeur de celui-ci change à chaque exécution du programme.

EDIT: c'est stupide, ne faites pas ça -> Si vous connaissez déjà le type de l'objet à désérialiser avec certitude, vous pouvez changer ces octets BitConvertes.GetBytes((int)typeof(yourtype).TypeHandle.Value)au moment de la désérialisation.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.