以前,我们将属性序列化为List<byte>
现在我们要将其更改为byte[]
。
理解您应该能够在版本之间自由交换集合类型,但我们得到ProtoBuf.ProtoException
[TestFixture, Category("Framework")]
class CollectionTypeChange
{
[Test]
public void TestRoundTrip()
{
var bytes = new List<byte>() {1,2,4};
var a = new ArrayHolder(bytes);
var aCopy = Deserialize<ArrayHolder>(Serialize(a));
//Passes
Assert.That(aCopy.CollectionOfBytes, Is.EquivalentTo(a.CollectionOfBytes));
}
[Test]
public void TestChangeArrayToList()
{
var bytes = new List<byte>() { 1, 2, 4 };
var a = new ArrayHolder(bytes);
var aCopy = Deserialize<ListHolder>(Serialize(a));
//Passes
Assert.That(aCopy.CollectionOfBytes, Is.EquivalentTo(a.CollectionOfBytes));
}
[Test]
public void TestChangeListToArray()
{
var bytes = new List<byte>() { 1, 2, 4 };
var a = new ListHolder(bytes);
//Throws: ProtoBuf.ProtoException : Invalid wire-type; this usually means you have over-written a file without truncating or setting the length; see http://stackoverflow.com/q/2152978/23354
var aCopy = Deserialize<ArrayHolder>(Serialize(a));
Assert.That(aCopy.CollectionOfBytes, Is.EquivalentTo(a.CollectionOfBytes));
}
public static byte[] Serialize<T>(T obj)
{
using (var stream = new MemoryStream())
{
Serializer.Serialize(stream, obj);
return stream.ToArray();
}
}
public static T Deserialize<T>(byte[] buffer)
{
using (var stream = new MemoryStream(buffer))
{
return Serializer.Deserialize<T>(stream);
}
}
}
[ProtoContract]
internal class ArrayHolder
{
private ArrayHolder()
{
CollectionOfBytes = new byte[0] {};
}
internal ArrayHolder(IEnumerable<byte> bytesToUse )
{
CollectionOfBytes = bytesToUse.ToArray();
}
[ProtoMember(1)]
public byte[] CollectionOfBytes { get; set; }
}
[ProtoContract]
internal class ListHolder
{
private ListHolder()
{
CollectionOfBytes = new List<byte>();
}
internal ListHolder(IEnumerable<byte> bytesToUse)
{
CollectionOfBytes = bytesToUse.ToList();
}
[ProtoMember(1)]
public List<byte> CollectionOfBytes { get; set; }
}
关于数组或字节是否有特殊之处,这意味着它不像我们预期的那样工作?
答案 0 :(得分:1)
这看起来是一个特别针对byte[]
属性的问题。如果我将属性类型更改为int []
和List<int>
,则行为不可重现。问题产生于以下事实:在协议缓冲区中有两种方法对数组进行编码:作为重复的键/值对,或"packed"作为具有长度分隔的值块的单个键。
对于字节数组,protobuf-net使用一个特殊的序列化器BlobSerializer
,它只是写入字节数组长度,然后将内容块作为打包重复字段复制到输出缓冲区中。它在读取时执行相反操作 - 当数据实际上处于重复键/值格式时不处理。
另一方面,List<byte>
使用通用ListDecorator
序列化。它的Read()
方法测试以查看当前在输入缓冲区中的格式并正确读取它 - 打包或解包。但是,它的Write()
方法会将字节数组默认解压缩。随后,当将缓冲区读入byte []
数组时,BlobSerializer
会抛出异常,因为格式不符合预期。可以说这是protobuf-net BlobSerializer
的一个错误。
但是,有一个简单的解决方法:通过设置 List<byte>
来声明IsPacked = true
应该以打包格式序列化:
[ProtoContract]
internal class ListHolder
{
private ListHolder()
{
CollectionOfBytes = new List<byte>();
}
internal ListHolder(IEnumerable<byte> bytesToUse)
{
CollectionOfBytes = bytesToUse.ToList();
}
[ProtoMember(1, IsPacked = true)]
public List<byte> CollectionOfBytes { get; set; }
}
这对于你的字节列表也应该是一个更紧凑的表示。
不幸的是,当字节集合包含设置了高位的字节时,上述解决方法失败。 Protobuf-net将打包的List<byte>
序列化为长度分隔的Base 128 Varints序列。因此,当高位设置的字节被串行化时,它被编码为两个字节。另一方面,byte []
成员像字符串一样序列化为length-delimited sequence of raw bytes。因此,字节数组中的一个字节始终在编码中编码为字节 - 这与List<byte>
的编码不兼容。
作为解决方法,可以使用List<byte>
类型中的私有代理ArrayHolder
属性:
[ProtoContract]
internal class ArrayHolder
{
private ArrayHolder()
{
CollectionOfBytes = new byte[0] { };
}
internal ArrayHolder(IEnumerable<byte> bytesToUse)
{
CollectionOfBytes = bytesToUse.ToArray();
}
[ProtoIgnore]
public byte[] CollectionOfBytes { get; set; }
[ProtoMember(1, OverwriteList = true)]
List<byte> ListOfBytes
{
get
{
if (CollectionOfBytes == null)
return null;
return new List<byte>(CollectionOfBytes);
}
set
{
if (value == null)
return;
CollectionOfBytes = value.ToArray();
}
}
}
示例fiddle。
或者,可以使用MetaType.SetSurrogate()
在(反)序列化期间用ArrayHolder
替换ListHolder
,如this answer中所示。