为什么“decimal”数据类型是非平坦的?

6
GCHandle.Alloc拒绝锁定包含“decimal”数据类型的结构体数组,但同时可以正常使用“double”。这是什么原因,我是否能够以某种方式解决它?
我知道我可以使用unsafe/fixed来获取指向数组的指针,但这无法与泛型一起使用。 :-(
完整的示例代码可演示该问题。第一个Alloc有效,但第二个会失败并显示“Object contains non-primitive or non-blittable data.”。
    public struct X1
    {
        public double X;
    }

    public struct X2
    {
        public decimal X;
    }

现在试试这个:
        var x1 = new[] {new X1 {X = 42}};
        var handle1 = GCHandle.Alloc(x1, GCHandleType.Pinned); // Works
        var x2 = new[] { new X2 { X = 42 } };
        var handle2 = GCHandle.Alloc(x2, GCHandleType.Pinned); // Fails

decimaldouble 非常不同。Double 是一种本地类型,而 decimal 是一个 128 位的结构。 - Panagiotis Kanavos
你也可以将double视为一个64位的结构。 - Dan Byström
1
这不是一个观点问题。Double是一种本地类型,就像“操作系统和CPU本地支持它”一样。Decimal只是一个结构,其内部布局使用4个DWORDs。相同的布局被OLE所使用(实际上这就是它来自的地方),它允许您与VB/VBA交换数据。但是C/C++不知道该怎么做。 - Panagiotis Kanavos
当然,你是正确的,但在这种情况下,我完全不关心数据表示或布局。我只想要一个指向原始数据块的指针。我可以使用上面代码中的“fixed(void* ptr = x1)”来实现。但我想找到一种通用的方法。就这样。结构体包含什么并不重要,只要它们里面没有引用即可。 - Dan Byström
GCHandle.Alloc 拒绝固定甚至 DateTime[]int?[],仅供参考... - xanatos
4个回答

19
  var handle2 = GCHandle.Alloc(x2, GCHandleType.Pinned);

运行时做了一个硬性假设,即您将调用handle.AddrOfPinnedObject()。否则很少有理由分配固定句柄。这将返回一个非托管指针,在C#中为IntPtr。与托管指针不同,您可以使用fixed关键字获取。
此外,它还假定您将向关心值大小和表示的代码传递此指针。但是,无法注入转换,该代码将直接对IntPtr进行操作。这需要值类型是可混合的,这是一个极客词汇,意思是值中的字节可以直接解释或复制,而无需任何转换,并且使用IntPtr的代码能够正确识别该值的可能性相当大。
那是一些.NET类型的问题,例如bool类型就很臭名昭著。只需尝试将此代码与decimal替换为bool,并注意您将获得完全相同的异常。System.Boolean是非常困难的Interop类型,没有主导标准来描述它应该是什么样子。在C语言和Winapi中它是4个字节,在COM Automation中是2个字节,在C++和其他几种语言中是1个字节。换句话说,“其他代码”能够解释1个字节的.NET值的可能性非常小。不可预测的大小特别讨厌,这会使所有后续成员都失效。

与 System.Decimal 类似,没有被广泛采用的标准来确定其内部格式。许多编程语言根本不支持它,尤其是 C 和 C++,如果你使用这样的语言编写代码,则需要使用库。该库可能使用 IEEE 754-2008 十进制浮点数,但这是一个新来者,也遭受“太多标准”的问题。在 CLI 规范编写时,IEEE 854-1987 标准已经存在,但被广泛忽视。今天仍然存在一个问题,几乎没有处理器设计支持十进制浮点数,我只知道 PowerPC 支持。

长话短说,您需要创建自己的可平移类型来存储十进制浮点数。.NET 设计师决定使用 COM Automation 的 Currency 类型来实现 System.Decimal,这是当时由于 Visual Basic 的主要实现。这极不可能改变,因为许多代码依赖于内部格式,使得这段代码最有可能兼容和快速:

    public struct X2 {
        private long nativeDecimal;
        public decimal X {
            get { return decimal.FromOACurrency(nativeDecimal); }
            set { nativeDecimal = decimal.ToOACurrency(value); }
        }
    }

您也可以考虑uint[]和Decimal.Get/SetBits(),但我认为它不太可能更快,您需要尝试。


2
如果你喜欢黑客技术(但是仅限于喜欢)(注意这个应该有效),请看下面的内容。
[StructLayout(LayoutKind.Explicit)]
public struct DecimalSplitted
{
    [FieldOffset(0)]
    public uint UInt0;
    [FieldOffset(4)]
    public uint UInt1;
    [FieldOffset(8)]
    public uint UInt2;
    [FieldOffset(12)]
    public uint UInt3;
}

[StructLayout(LayoutKind.Explicit)]
public struct DecimalToUint
{
    [FieldOffset(0)]
    public DecimalSplitted Splitted;
    [FieldOffset(0)]
    public decimal Decimal;
}

[StructLayout(LayoutKind.Explicit)]
public struct StructConverter
{
    [FieldOffset(0)]
    public decimal[] Decimals;

    [FieldOffset(0)]
    public DecimalSplitted[] Splitted;
}

然后:
var decimals = new decimal[] { 1M, 2M, decimal.MaxValue, decimal.MinValue };

DecimalSplitted[] asUints = new StructConverter { Decimals = decimals }.Splitted;

// Works correctly
var h = GCHandle.Alloc(asUints, GCHandleType.Pinned);

// But here we don't need it :-)
h.Free();

for (int i = 0; i < asUints.Length; i++)
{
    DecimalSplitted ds = new DecimalSplitted
    {
        UInt0 = asUints[i].UInt0,
        UInt1 = asUints[i].UInt1,
        UInt2 = asUints[i].UInt2,
        UInt3 = asUints[i].UInt3,
    };

    Console.WriteLine(new DecimalToUint { Splitted = ds }.Decimal);
}

我同时使用了两种相当著名的技巧:使用[StructLayout(LayoutKind.Explicit)],您可以像C联合一样叠加两个值类型甚至是两个值类型数组。最后一个有问题: Length没有被“重新计算”,因此如果您将一个byte[]与一个long[]叠加在一起,如果您放置一个包含8个字节的数组,则两个字段都将显示长度为8,但如果您尝试访问long[1],程序将崩溃。 在这种情况下,这不是问题,因为这两个结构具有相同的sizeof
请注意,我使用了4个uint,但我也可以使用2个ulong或16个byte

我知道这些,但这并不是我想要的。不过我还是点了赞,因为它既酷又有用! :-) - Dan Byström

0
可位化类型在托管代码和非托管代码之间传递时不需要转换。MSDN 但是,对于 Decimal 类型则不是这样。其他应用程序或使用您的数据的人如何能够理解 Decimal 结构呢?解决方法是将 Decimal 分解为 2 个整数,一个表示数字,一个表示小数基数,例如将 12.34 分解为 1234 和 2(即 1234 / 10^2)。
要正确将 Decimal 转换为二进制,请使用 GetBits 方法,相反操作有点棘手,this page 中有示例。

解释是正确的,但解决方法行不通 - 十进制需要四个32位整数,因为数字使用96位。您可以直接从十进制中使用Decimal.GetBits获取整数数组。 - Panagiotis Kanavos
没有其他应用会使用这个。我只想将数组写入/从磁盘读取。这比其他序列化方法(如Protobuf)要快得多。我很惊讶十进制数不起作用。 - Dan Byström
@PanagiotisKanavos 你说得没错,但对于较小的情况,int/long 应该就足够了。 - Andrey
@Andrey 在这种情况下,你不会传输小数,而是将它们转换为不同的表示形式,并存在舍入和缩放误差。仅传输这128位将比进行转换快得多。 - Panagiotis Kanavos
我的解决办法可能是在涉及到十进制时采用unsafe/fixed,但在其他情况下保持通用。 - Dan Byström
显示剩余2条评论

-1
这段代码(从另一个我现在找不到的 SO 问题重构而来)使用 decimal[] 完全没有问题。你的问题在于decimal 是可引用类型(blittable),但不是原始类型,包含非原始类型的结构体是无法固定(pinnable)的(GCHandle.Alloc 显示错误“对象包含非原始或非可引用数据。”)。

为什么 decimal 不是原始类型?

/// <summary>
/// Helper class for generic array pointers
/// </summary>
/// <typeparam name="T"></typeparam>
internal class GenericArrayPinner<T> : IDisposable
{
    GCHandle _pinnedArray;
    private T[] _arr;
    public GenericArrayPinner(T[] arr)
    {
        _pinnedArray = GCHandle.Alloc(arr, GCHandleType.Pinned);
        _arr = arr;
    }
    public static implicit operator IntPtr(GenericArrayPinner<T> ap)
    {

        return ap._pinnedArray.AddrOfPinnedObject();
    }

    /// <summary>
    /// Get unmanaged poinetr to the nth element of generic array
    /// </summary>
    /// <param name="n"></param>
    /// <returns></returns>
    public IntPtr GetNthPointer(int n)
    {
        return Marshal.UnsafeAddrOfPinnedArrayElement(this._arr, n);
    }

    public void Dispose()
    {
        _pinnedArray.Free();
        _arr = null;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接