Cách chuyển đổi một đối tượng thành một mảng byte trong C #


99

Tôi có một bộ sưu tập các đối tượng mà tôi cần ghi vào tệp nhị phân.

Tôi cần các byte trong tệp nhỏ gọn, vì vậy tôi không thể sử dụng BinaryFormatter. BinaryFormattercung cấp tất cả các loại thông tin cho nhu cầu giải trí.

Nếu tôi cố gắng

byte[] myBytes = (byte[]) myObject 

Tôi nhận được một ngoại lệ thời gian chạy.

Tôi cần điều này nhanh chóng vì vậy tôi không muốn sao chép các mảng byte xung quanh. Tôi chỉ muốn dàn diễn viên byte[] myBytes = (byte[]) myObjectlàm việc!

OK, nói rõ là tôi không thể có bất kỳ siêu dữ liệu nào trong tệp đầu ra. Chỉ các byte đối tượng. Đối tượng-đối tượng được đóng gói. Dựa trên câu trả lời nhận được, có vẻ như tôi sẽ viết Buffer.BlockCopymã cấp thấp . Có lẽ đang sử dụng mã không an toàn.

Câu trả lời:


173

Để chuyển đổi một đối tượng thành một mảng byte:

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

Bạn chỉ cần sao chép hàm này vào mã của mình và gửi cho nó đối tượng mà bạn cần chuyển đổi thành một mảng byte. Nếu bạn cần chuyển đổi lại mảng byte thành một đối tượng, bạn có thể sử dụng hàm bên dưới:

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

Bạn có thể sử dụng các hàm này với các lớp tùy chỉnh. Bạn chỉ cần thêm [Serializable]thuộc tính trong lớp của mình để kích hoạt tuần tự hóa


9
Tôi đã thử điều này và nó đã thêm tất cả các loại siêu dữ liệu. OP cho biết ông không muốn siêu dữ liệu.
user316117

4
Chưa kể, mọi người dường như cho rằng những gì bạn đang cố gắng đăng nhiều kỳ là thứ mà bạn đã viết, hoặc đã được thiết lập trước để được đăng nhiều kỳ.
Hexum064,

3
Bạn có thể truyền trực tiếp mảng byte tới hằng số MemoryStreamtrong ví dụ mã thứ hai. Điều này sẽ loại bỏ việc sử dụng Write(...)Seek(...).
rõ6656

41

Nếu bạn muốn dữ liệu được tuần tự hóa thực sự nhỏ gọn, bạn có thể tự viết các phương pháp tuần tự hóa. Bằng cách đó, bạn sẽ có chi phí tối thiểu.

Thí dụ:

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

}

Tôi có một số int để viết và một số chuỗi?
Smith

1
@Smith: Vâng, bạn có thể làm điều đó, chỉ cần viết chúng sau nhau. Người BinaryWritersẽ viết chúng ở định dạng mà người BinaryReadercó thể đọc, miễn là bạn viết và đọc chúng theo cùng một thứ tự.
Guffa

1
sự khác biệt giữa BinaryWriter/Readervà sử dụng aBinaryFormatter
Smith

3
@Smith: Bằng cách sử dụng, BinaryWriter/Readerbạn tự mình thực hiện tuần tự hóa / deserialisation và bạn có thể chỉ ghi / đọc dữ liệu thực sự cần thiết, càng nhỏ gọn càng tốt. Các BinaryFormattersử dụng phản ánh để tìm hiểu dữ liệu gì để ghi / đọc, và sử dụng một định dạng mà các công trình cho tất cả các trường hợp có thể. Nó cũng bao gồm thông tin meta về định dạng trong luồng, để tăng thêm chi phí.
Guffa

1
@Smith: Bạn có thể ép kiểu enum tới int(hoặc nếu bạn đã chỉ định bất kỳ kiểu nào khác làm nơi lưu trữ cho enum) và viết nó. Khi bạn đọc nó, bạn có thể truyền nó sang kiểu enum.
Guffa

31

Vâng, một dàn diễn viên từ myObjectđến byte[]sẽ không bao giờ hoạt động trừ khi bạn có một chuyển đổi rõ ràng hoặc nếu myObject một byte[]. Bạn cần một số loại khung tuần tự hóa . Có rất nhiều thứ ở đó, bao gồm cả Bộ đệm giao thức ở gần và thân thiết với tôi. Nó khá "gọn gàng và có ý nghĩa" cả về không gian và thời gian.

Tuy nhiên, bạn sẽ thấy rằng hầu hết tất cả các khuôn khổ tuần tự hóa đều có những hạn chế đáng kể về những gì bạn có thể tuần tự hóa - Bộ đệm giao thức nhiều hơn một số, do là nền tảng chéo.

Nếu bạn có thể đưa ra nhiều yêu cầu hơn, chúng tôi có thể giúp bạn nhiều hơn - nhưng sẽ không bao giờ đơn giản như đúc ...

CHỈNH SỬA: Chỉ để trả lời điều này:

Tôi cần tệp nhị phân của mình để chứa các byte của đối tượng. Chỉ các byte, không có siêu dữ liệu nào. Đối tượng-đối tượng được đóng gói. Vì vậy, tôi sẽ thực hiện tuần tự hóa tùy chỉnh.

Xin lưu ý rằng các byte trong các đối tượng của bạn thường là các tham chiếu ... vì vậy bạn sẽ cần phải tìm ra những gì cần làm với chúng.

Tôi nghi ngờ rằng bạn sẽ thấy rằng việc thiết kế và triển khai khuôn khổ tuần tự hóa tùy chỉnh của riêng bạn khó hơn bạn tưởng tượng.

Cá nhân tôi khuyên rằng nếu bạn chỉ cần làm điều này cho một số loại cụ thể, bạn không cần cố gắng tìm ra một khuôn khổ tuần tự hóa chung. Chỉ cần triển khai một phương thức phiên bản và một phương thức tĩnh trong tất cả các kiểu bạn cần:

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

Một điều cần lưu ý: mọi thứ trở nên phức tạp hơn nếu bạn có liên quan đến tài sản thừa kế. Không cần kế thừa, nếu bạn biết loại bạn đang bắt đầu, bạn không cần bao gồm bất kỳ thông tin loại nào. Tất nhiên, cũng có vấn đề về lập phiên bản - bạn có cần lo lắng về khả năng tương thích ngược và chuyển tiếp với các phiên bản khác nhau của loại của bạn không?


Tôi gọi đây là "protobuf-csharp-port" (Google-code) hay "dotnet-protobufs" (Git) có đúng hơn không?
Marc Gravell

1
Tôi cần tệp nhị phân của mình để chứa các byte của đối tượng. Chỉ các byte, không có siêu dữ liệu nào. Đối tượng-đối tượng được đóng gói. Vì vậy, tôi sẽ thực hiện tuần tự hóa tùy chỉnh.
chuckhlogan 18/09/09

6
Rủi ro của siêu dữ liệu bằng không là khi đó bạn rất khó dung nạp phiên bản, vì nó có rất ít cách cho phép linh hoạt trước khi quá muộn. Bộ đệm giao thức khá dày đặc dữ liệu. Bạn có thực sự cần thêm vòng vặn đó không?
Marc Gravell

@Marc: Và dĩ nhiên cho số nguyên, PB có thể kết thúc được dày đặc hơn so với các byte thô ...
Jon Skeet

16

Tôi lấy câu trả lời của Crystalonics và biến chúng thành các phương pháp mở rộng. Tôi hy vọng ai đó sẽ thấy chúng hữu ích:

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
Điều này thực sự hữu ích và dễ dàng !! Cảm ơn bạn.
MrHIDEn

13

Bạn đang thực sự nói về tuần tự hóa, có thể có nhiều hình thức. Vì bạn muốn nhỏ và nhị phân, bộ đệm giao thức có thể là một lựa chọn khả thi - cung cấp khả năng chịu đựng và tính di động của phiên bản. Không giống như BinaryFormatter, định dạng dây của bộ đệm giao thức không bao gồm tất cả các loại siêu dữ liệu; chỉ là những điểm đánh dấu rất ngắn gọn để xác định dữ liệu.

Trong .NET có một số triển khai; đặc biệt

Tôi khiêm tốn tranh luận rằng protobuf-net (mà tôi đã viết) cho phép sử dụng nhiều .NET-idiomatic hơn với các lớp C # điển hình (bộ đệm giao thức "thông thường" có xu hướng yêu cầu tạo mã); ví dụ:

[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
Ngay cả "điểm đánh dấu ngắn" vẫn là siêu dữ liệu. Sự hiểu biết của tôi về những gì OP muốn không gì khác ngoài dữ liệu trong đối tượng. Vì vậy, ví dụ, nếu đối tượng là một cấu trúc có 2 số nguyên 32 bit, thì anh ta sẽ mong đợi kết quả là một mảng byte 8 byte.
user316117

@ user316117, đó là một nỗi đau thực sự cho việc tạo phiên bản. Mỗi cách tiếp cận đều có ưu điểm và nhược điểm.
Marc Gravell


Có một cách để tránh sử dụng các thuộc tính Proto *? Các thực thể tôi muốn sử dụng nằm trong thư viện của bên thứ ba.
Alex 75

5

Điều này đã làm việc cho tôi:

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

foo là một Đối tượng mà tôi chắc chắn 100% rằng đó là một mảng byte.


2

Hãy xem Serialization , một kỹ thuật để "chuyển đổi" toàn bộ một đối tượng thành một luồng byte. Bạn có thể gửi nó vào mạng hoặc ghi nó vào một tệp và sau đó khôi phục nó trở lại một đối tượng sau đó.


Tôi nghĩ rằng chuckhlogan đã từ chối điều đó một cách rõ ràng (Formatter == Serialization).
Henk Holterman

@Henk - nó phụ thuộc vào lý do là gì; anh ấy đã đề cập đến thông tin bổ sung, mà tôi sử dụng để nhập siêu dữ liệu và thông tin trường; bạn có thể sử dụng tuần tự hóa mà không cần chi phí đó; chỉ là không với BinaryFormatter.
Marc Gravell

2

Tôi đã tìm thấy một cách khác để chuyển đổi một đối tượng thành byte [], đây là giải pháp của tôi:

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

Trân trọng


1

Để truy cập trực tiếp vào bộ nhớ của một đối tượng (để thực hiện "kết xuất lõi"), bạn sẽ cần phải truy cập vào mã không an toàn.

Nếu bạn muốn thứ gì đó nhỏ gọn hơn BinaryWriter hoặc một kết xuất bộ nhớ thô sẽ cung cấp cho bạn, thì bạn cần viết một số mã tuần tự hóa tùy chỉnh trích xuất thông tin quan trọng từ đối tượng và đóng gói nó theo cách tối ưu.

chỉnh sửa PS Rất dễ dàng bọc cách tiếp cận BinaryWriter thành một DeflateStream để nén dữ liệu, thường sẽ giảm khoảng một nửa kích thước của dữ liệu.


1
Mã không an toàn là không đủ. C # và CLR vẫn sẽ không cho phép bạn đưa một con trỏ thô đến một đối tượng được quản lý ngay cả trong mã không an toàn hoặc đặt hai tham chiếu đối tượng trong một liên hợp.
Pavel Minaev 18/09/09

0

Tôi tin rằng những gì bạn đang cố gắng làm là không thể.

Phần rác BinaryFormattertạo ra là cần thiết để khôi phục đối tượng từ tệp sau khi chương trình của bạn dừng.
Tuy nhiên, có thể lấy dữ liệu đối tượng, bạn chỉ cần biết kích thước chính xác của nó (khó hơn là nó nghe):

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

điều này có thể được phục hồi thông qua:

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

Lý do tại sao các phương thức trên không hoạt động cho tuần tự hóa là bốn byte đầu tiên trong dữ liệu trả về tương ứng với a RuntimeTypeHandle. Mô RuntimeTypeHandletả bố cục / kiểu của đối tượng nhưng giá trị của nó thay đổi mỗi khi chương trình được chạy.

CHỈNH SỬA: điều đó thật ngu ngốc, đừng làm vậy -> Nếu bạn đã biết loại đối tượng cần giải không khí nhất định, bạn có thể chuyển các byte đó BitConvertes.GetBytes((int)typeof(yourtype).TypeHandle.Value)tại thời điểm giải không khí hóa.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.