【问题标题】:*(decimal*)d=XXXm results in another output than BinaryWriter.Write(XXXm)*(decimal*)d=XXXm 导致另一个输出而不是 BinaryWriter.Write(XXXm)
【发布时间】:2019-05-29 05:10:41
【问题描述】:

我正在编写一个优化的二进制读取器/写入器,用于我自己的学习目的。一切正常,直到我编写了decimals 的编码和解码测试。我的测试还包括 .NET Framework 的 BinaryWriter 是否为我的 BinaryWriter 产生兼容的输出,反之亦然。

我主要使用不安全和指针将变量写入字节数组。这些是通过指针和BinaryWriter 写入小数时的结果:

BinaryWriter....: E9 A8 94 23 9B CA 4E 44 63 C5 44 39 00 00 1A 00
unsafe *decimal=: 00 00 1A 00 63 C5 44 39 E9 A8 94 23 9B CA 4E 44

我编写小数的代码如下所示:

unsafe
{
    byte[] data = new byte[16];

    fixed (byte* pData = data)
        *(decimal*)pData = 177.237846528973465289734658334m;
}

而使用 .NET Framework 的 BinaryWriter 看起来像这样:

using (MemoryStream ms = new MemoryStream())
{
    using (BinaryWriter writer = new BinaryWriter(ms))
        writer.Write(177.237846528973465289734658334m);

    ms.ToArray();
}

Microsoft 使他们的 BinaryWriterdecimals 在内存中的存储方式不兼容。通过查看参考源,我们看到微软使用了一个名为 GetBytes 的内部方法,这意味着 GetBytes 的输出与 decimals 在内存中的存储方式不兼容。

微软以这种方式实现写入decimals 有什么原因吗?使用unsafe 的方式来实现自己的二进制格式或协议会不会很危险,因为将来小数的内部布局可能会发生变化?

使用unsafe 方式比使用BinaryWriter 调用的GetBytes 方式执行得更好。

【问题讨论】:

  • @GSerg 公平地说,引用的答案只是说它是.NET 特定格式。它并没有真正回答 OP 的问题。
  • @Neijwiert 好吧,没有人可以回答 OP 的问题,即微软是否会愿意改变这种格式。出于兼容性原因,我们只能推测这是极不可能的。
  • @GSerg 是的,但我有点希望有人解释当前的实现是如何完成的,因为我不能。
  • @Neijwiert 请参阅second answerdecimal --> decimal.GetBytes(), 16 bytes, should see the System.Decimal class code。这是一个错字,应该是GetBits()

标签: c# decimal unsafe binarywriter


【解决方案1】:

Microsoft 本身试图保持decimal 及其组件的对齐尽可能稳定。您还可以在提到的 .NET 框架的参考源中看到这一点:

// NOTE: Do not change the order in which these fields are declared. The
// native methods in this class rely on this particular order.
private int flags;
private int hi;
private int lo;
private int mid;

连同[StructLayout(LayoutKind.Sequential)] 的使用,结构在内存中完全以这种方式对齐。

由于GetBytes 方法使用在内部构建decimal 数据的变量按照它们在结构本身中对齐的顺序,您会得到错误的结果:

internal static void GetBytes(Decimal d, byte[] buffer)
{
    Contract.Requires((buffer != null && buffer.Length >= 16), "[GetBytes]buffer != null && buffer.Length >= 16");
    buffer[0] = (byte)d.lo;
    buffer[1] = (byte)(d.lo >> 8);
    buffer[2] = (byte)(d.lo >> 16);
    buffer[3] = (byte)(d.lo >> 24);

    buffer[4] = (byte)d.mid;
    buffer[5] = (byte)(d.mid >> 8);
    buffer[6] = (byte)(d.mid >> 16);
    buffer[7] = (byte)(d.mid >> 24);

    buffer[8] = (byte)d.hi;
    buffer[9] = (byte)(d.hi >> 8);
    buffer[10] = (byte)(d.hi >> 16);
    buffer[11] = (byte)(d.hi >> 24);

    buffer[12] = (byte)d.flags;
    buffer[13] = (byte)(d.flags >> 8);
    buffer[14] = (byte)(d.flags >> 16);
    buffer[15] = (byte)(d.flags >> 24);
}

在我看来,相应的 .NET 开发人员试图将 GetBytes 提供的格式调整为小端,但犯了一个错误。他不仅订购了decimal 的组件的字节,还订购了组件本身。 (flags, hi, lo, mid 变成 lo, mid, hi, flags。)但是小端布局只适用于字段而不是整个structs - 特别是[StructLayout(LayoutKind.Sequential)]

我的建议通常是使用 Microsoft 在其课程中提供的方法。因此,与使用 unsafe 相比,我更喜欢任何基于 GetBytesGetBits 的数据序列化方式,因为 Microsoft 将以任何方式保持与 BinaryWriter 的兼容性。但是,cmets 有点严重,我不希望微软在这个非常基础的层面上打破 .NET 框架。

我很难相信性能比GetBits 更偏向于unsafe。毕竟我们在这里谈论的是decimals。您仍然可以通过unsafeGetBitsint 推送到您的byte[]

【讨论】:

  • decimal 的内部结构记录在Decimal(Int32[]) constructor 中。它还解释了为什么decimal.GetBits() 按特定顺序返回一个由四个ints 组成的数组,以及它们的含义。我看不出哪里会出现字节顺序不正确的错误。
  • @GSerg 正如在其他 cmets 中已经提到的:GetBits() 与此无关,因为BinaryWriter uses GetBytes() internally。此外,链接文档没有告诉 为什么 structure 的内存布局与 GetBytes() 返回的布局不同,而我的回答 确实
  • 你的推理倒退了。 GetBits() 是起点,因为它已记录在案。它返回的内容不能改变。从这个起点我们可以看到internal GetBytes() 返回与记录 GetBits() 相同的数据,但始终采用小端格式(而GetBits() 自然会使用系统的当前字节序) -这对于具有不同字节顺序的系统之间的可移植性是有意义的。这两种方法不会改变(这会破坏在假设改变之前将decimals 加载到存储中的能力)。
  • 相反,包含 decimal 结构的私有字段的内部顺序可能随时容易改变,因为这纯粹是框架内部的 - 但实际上它不会发生,因为没有理由做出这样的改变(例如引入新字段会破坏GetBitsGetBytes;如果必须这样做,他们会想出一个新类型,例如decimal2)。但即使在这种情况下,严格的答案是否定的,不能保证您通过将decimal* 转换为byte* 看到的内容不会改变,您不应该依赖它。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-05-31
  • 1970-01-01
  • 2021-09-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-11
  • 1970-01-01
相关资源
最近更新 更多