使用自定义十进制原型合约(C#/C++ 互操作)时,Protobuf-net(反)序列化小数抛出
Posted
技术标签:
【中文标题】使用自定义十进制原型合约(C#/C++ 互操作)时,Protobuf-net(反)序列化小数抛出【英文标题】:Protobuf-net (de)serialization of decimals throws when using custom decimal proto contract (C#/C++ interop) 【发布时间】:2013-05-15 08:45:57 【问题描述】:假设我要序列化,然后使用 protobuf-net 反序列化一个小数:
const decimal originalDecimal = 1.6641007661819458m;
using (var memoryStream = new MemoryStream())
Serializer.Serialize(memoryStream, originalDecimal);
memoryStream.Position = 0;
var deserializedDecimal = Serializer.Deserialize<decimal>(memoryStream);
Assert.AreEqual(originalDecimal, deserializedDecimal);
它工作正常。 Protobuf-net 在内部使用以下小数表示(参见Bcl.proto):
message Decimal
optional uint64 lo = 1; // the first 64 bits of the underlying value
optional uint32 hi = 2; // the last 32 bis of the underlying value
optional sint32 signScale = 3; // the number of decimal digits, and the sign
现在说我通过代码定义了一个假定等效的原型合约:
[ProtoContract]
public class MyDecimal
[ProtoMember(1, IsRequired = false)]
public ulong Lo;
[ProtoMember(2, IsRequired = false)]
public uint Hi;
[ProtoMember(3, IsRequired = false)]
public int SignScale;
...那么我不能序列化一个decimal
并返回一个MyDecimal
,也不能序列化一个MyDecimal
并返回一个decimal
。
从decimal
到MyDecimal
:
const decimal originalDecimal = 1.6641007661819458m;
using (var memoryStream = new MemoryStream())
Serializer.Serialize(memoryStream, originalDecimal);
memoryStream.Position = 0;
// following line throws a Invalid wire-type ProtoException
Serializer.Deserialize<MyDecimal>(memoryStream);
从MyDecimal
到decimal
:
var myDecimal = new MyDecimal
Lo = 0x003b1ee886632642,
Hi = 0x00000000,
SignScale = 0x00000020,
;
using (var memoryStream = new MemoryStream())
Serializer.Serialize(memoryStream, myDecimal);
memoryStream.Position = 0;
// following line throws a Invalid wire-type ProtoException
Serializer.Deserialize<decimal>(memoryStream);
我在这里遗漏了什么吗?
我正在开发一个 C++ 应用程序,该应用程序需要通过协议缓冲区与 C# 进行通信,但不知道十进制反序列化失败的原因。
【问题讨论】:
【参考方案1】:这是“它是一个对象?还是一个裸值?”的边缘情况。你不能只是在 protobuf 中序列化 int
,你需要一个包装器对象。因此,对于裸值,它假装该值实际上是假设的包装对象的字段 1。但是,在decimal
的情况下,这有点棘手——因为decimal
实际上被编码为好像它是一个对象。所以技术上decimal
可以写成一个裸值......但是:它看起来不是(它正在包装它) - 我怀疑在这个阶段纠正它是一个好主意。
基本上,如果不是序列化一个裸值,而是序列化一个 具有 值的 对象,这将更加可靠。它也会更有效地工作(protobuf-net 查找它知道的类型,裸值非常适合备用方案)。例如:
[ProtoContract]
class DecimalWrapper
[ProtoMember(1)]
public decimal Value get; set;
[ProtoContract]
class MyDecimalWrapper
[ProtoMember(1)]
public MyDecimal Value get; set;
如果我们序列化这些,它们是 100% 可互换的:
const decimal originalDecimal = 1.6641007661819458m;
using (var memoryStream = new MemoryStream())
var obj = new DecimalWrapper Value = originalDecimal ;
Serializer.Serialize(memoryStream, obj);
// or, as it happens (see text) - this is equal to
// Serializer.Serialize(memoryStream, originalDecimal);
memoryStream.Position = 0;
var obj2 = Serializer.Deserialize<MyDecimalWrapper>(memoryStream);
Console.WriteLine("0, 1, 2",
obj2.Value.Lo, obj2.Value.Hi, obj2.Value.SignScale);
// ^^^ 16641007661819458, 0, 32
memoryStream.SetLength(0);
Serializer.Serialize(memoryStream, obj2);
memoryStream.Position = 0;
var obj3 = Serializer.Deserialize<DecimalWrapper>(memoryStream);
bool eq = obj3.Value == obj.Value; // True
实际上,因为 protobuf-net 假装有一个对象,所以说Serialize<decimal>
将与Serialize<MyDecimalWrapper>
100% 兼容也是正确的,但为了您自己的理智,它可能是只是更容易坚持简单的“始终序列化 DTO 实例”方法,而不必考虑“这是 DTO?还是裸值?”
作为最后的想法:如果您使用互操作,我建议避免使用decimal
,因为 protobuf 规范中没有定义,而且不同平台的“十进制”类型通常具有不同的含义。 protobuf-net 发明了一个含义,主要是为了允许 protobuf-net 往返(到自己)更广泛的 DTO,但是将这个值解析到任意平台可能会很尴尬。在跨平台工作并使用decimal
时,我建议考虑 诸如double
/float
之类的东西,或者通过long
/ulong
进行一些固定精度,或者甚至只是@987654335 @。
【讨论】:
感谢您的回答。这一切都是有道理的。一些 cmets: 1. 我们不发送裸小数,我们在 DTO 中使用它们。 2. 是的,我的样本并不能完全说明我遇到的问题;事实上,我发现包含decimal
的DecimalWrapper
与包含MyDecimal
的MyDecimalWrapper
之间的序列化/反序列化按预期工作。 3. 就我而言,我有一个 C++ 中的小数,可以表示为与 C# 兼容的 lo/hi/signscale; protobuf-net 十进制看起来像一个很好的合同。
4.问题似乎是由于 bcl.proto 十进制是用 sint32
SignScale 定义的,并且 protobuf-net 正在向/从流中写入/读取 uint32
。如果序列化发生在 C++ 中(sint32
的写入),那么 protobuf-net 读取它的方式会产生无效值。我什至不确定如何在 C# 中处理 sint32
值。
@RomainVerdier 啊,sint32
vs uint32
...该死的,这很痛苦。 zig-zag 编码很简单,但如果我让合同和代码说不同的东西,那就很烦人了
@RomainVerdier 嗯......这里的主要“修复”可能是更改 bcl.proto 以宣传 uint32
。很好奇。
@RomainVerdier 啊,我觉得uint
很有道理,其实;它在这里的工作方式是将符号编码为位 0,将比例编码为位 1-16 - 所以只使用前 16 位并且它始终为正; sint32
效率低下以上是关于使用自定义十进制原型合约(C#/C++ 互操作)时,Protobuf-net(反)序列化小数抛出的主要内容,如果未能解决你的问题,请参考以下文章