C# Protobuf-net:小数字典:零不能正确往返

Posted

技术标签:

【中文标题】C# Protobuf-net:小数字典:零不能正确往返【英文标题】:C# Protobuf-net: Dictionary of decimals: Zeroes don't get roundtrip properly 【发布时间】:2018-06-12 00:16:09 【问题描述】:

我在 protobuf-net 中发现了一个关于十进制零序列化/反序列化的奇怪错误,想知道是否有人找到了解决此问题的好方法,或者这实际上是一个功能。

给定上面的字典,如果我在 linqpad 中运行:

void Main()

    
        Dictionary<string, decimal> dict = new Dictionary<string, decimal>();
        dict.Add("one", 0.0000000m);
        DumpStreamed(dict);
    

    
        Dictionary<string, decimal> dict = new Dictionary<string, decimal>();
        dict.Add("one", 0m);
        DumpStreamed(dict);
    


public static void DumpStreamed<T>(T val)

    using (var stream = new MemoryStream())
    
        Console.Write("Stream1: ");
        ProtoBuf.Serializer.Serialize(stream, val);
        foreach (var by in stream.ToArray())
        
            Console.Write(by);
        

        Console.WriteLine();
        Console.Write("Stream2: ");
        stream.Position = 0;
        var item = ProtoBuf.Serializer.Deserialize<T>(stream);
        using(var stream2 = new MemoryStream())
        
            ProtoBuf.Serializer.Serialize(stream2, item);
            foreach (var by in stream2.ToArray())
            
                Console.Write(by);
            

        
    

    Console.WriteLine();
    Console.WriteLine("----");

我会得到两个不同的流:

第一次序列化:1091031111101011822414

二次序列化:107103111110101180

(0.0000000m 在反序列化时被转换为 0)。

我发现这是由于 ReadDecimal 中的这行代码:

 if (low == 0 && high == 0) return decimal.Zero;

有谁知道为什么零只在反序列化过程中被标准化,而不是在序列化过程中?

或者在序列化/反序列化的字典中始终规范化或始终不规范化十进制零的任何解决方法?

【问题讨论】:

添加了完整的代码块,对此感到抱歉。重要性不在于保留零,而在于生成的字节流的差异。产生的字节流不同,导致字典周围的误报也不同。 (交叉引用“长尾”的github问题:github.com/mgravell/protobuf-net/issues/402) 【参考方案1】:

是的;问题在于这条善意但可能有害的行:

    if (low == 0 && high == 0) return decimal.Zero;

忽略检查signScale。确实应该是:

    if (low == 0 && high == 0 && signScale == 0) return decimal.Zero;

我会为下一次构建调整它。

(编辑:我最终完全删除了该检查 - 其余代码只是一些整数移位等,因此拥有“分支”可能比 没有拥有它更昂贵)

【讨论】:

【参考方案2】:

浮点数据类型实际上是具有多个元素的结构。其中包括基值和要提高基值的指数。 decimal 的 c# 文档声明如下:

十进制数的二进制表示由一个 1 位符号、一个 96 位整数和一个比例因子组成,该比例因子用于划分整数并指定它的哪一部分是小数部分。比例因子是隐含的数字 10,指数范围从 0 到 28

例如,您可以将 1234000 表示为

基础值为 1234000 x 10 ^ 0 基础值为 123000 x 10 ^1 基础值为 12300 x 10 ^ 2

等等

所以这个问题不仅限于零。所有十进制值都可以用一种以上的方式表示。如果您依靠字节流来检查等价性,那么您会遇到很多问题。你真的不应该这样做,因为你肯定会得到误报,而不仅仅是零。

至于序列化时的规范化,我认为这是 ProtoBuf 特有的问题。您当然可以编写自己的序列化程序,该序列化程序会采取措施对数据进行规范化,尽管可能很难弄清楚。另一种选择是在存储之前将小数转换为一些自定义类,或者将它们存储为它们的字符串表示形式,这听起来很奇怪。

如果您有兴趣处理一些小数并检查原始数据,请参阅GetBits() 方法。或者您可以使用此扩展方法查看内存中的表示并亲自查看:

public static unsafe string ToBinaryHex(this decimal This)

    byte* pb = (byte*)&This;
    var bytes = Enumerable.Range(0, 16).Select(i => (*(pb + i)).ToString("X2"));
    return string.Join("-", bytes);

【讨论】:

虽然这都是 true,但它与 IMO 的问题并不真正相关; protobuf-net 已经在使用 GetBits 等,并且知道规模。 嗨@MarcGravell。我认为您正在阅读我的回答,就好像它是写给您的一样。它是针对 OP 的,他们将二进制相等与数值相等混为一谈,可能会带来灾难性的后果。 我真的不认为它们是(将二进制相等与数值相等混为一谈);他们只是希望它可以可预测地往返,这是非常合理的 查看评论 重要的不是保留零,而是生成的字节流的差异。产生的字节流不同,导致字典周围的误报不同。请考虑在您已经非常出色的答案中评论这方面。 说实话,我更关心的是它默默地改变了数据;这实际上不是误报——而是真正的肯定:数据不同(无效);你在这里的回答解释了为什么他们可能认为这是一个误报,而实际上它是一个真正的肯定,但是......我宁愿修复代码以使其成为一个真正的否定:)

以上是关于C# Protobuf-net:小数字典:零不能正确往返的主要内容,如果未能解决你的问题,请参考以下文章

protobuf-net - 反引号、字典和 .proto 文件

C#常用正则表达式符号大全

asp.net怎么限制文本框输入的类型c#

C#文本框的文本,正则表达式约束,正整数和小数点后只有一位小数

c#中怎样去掉小数点和小数点末尾的0,但是不能去掉整数后面的0

double 在 C# 中显示 2 个非零小数? [复制]