如何有效地确保一个十进制值至少有 N 位小数

How to efficiently ensure a decimal value has at least N decimal places

我想在进行算术运算之前有效地确保十进制值至少有 N(在下面的示例中为 3)位。

显然我可以用 "0.000######....#" 格式化然后解析,但它的效率相对较低,我正在寻找一种避免将 to/from 转换为字符串的解决方案。

我尝试了以下解决方案:

decimal d = 1.23M;
d = d + 1.000M - 1;
Console.WriteLine("Result = " + d.ToString()); // 1.230

当在 Debug 和 Release 版本中使用 Visual Studio 2015 编译时,它似乎适用于所有值 <= Decimal.MaxValue - 1

但我一直怀疑编译器可能被允许优化 (1.000 - 1)。 C# 规范中是否有任何内容可以保证这将始终有效?

或者有更好的解决方案,例如使用 Decimal.GetBits?

更新

根据 Jon Skeet 的回答,我之前曾尝试添加 0.000M,但这在 dotnetfiddle 上不起作用。所以我惊讶地发现 Decimal.Add(d, 0.000M) 确实有效。 Here's a dotnetfiddle 比较 d + 000Mdecimal.Add(d,0.000M):结果与 dotnetfiddle 不同,但使用 Visual Studio 2015:

编译相同代码时相同
decimal d = 1.23M;
decimal r1 = decimal.Add(d, 0.000M);
decimal r2 = d + 0.000M;
Console.WriteLine("Result1 = " + r1.ToString());  // 1.230 
Console.WriteLine("Result2 = " + r2.ToString());  // 1.23 on dotnetfiddle

所以至少某些行为似乎依赖于编译器,这并不令人放心。

如果您担心编译器会优化运算符(尽管我怀疑它是否会这样做),您可以直接调用 Add 方法。请注意,您不需要先加后减 - 您只需添加 0.000m。例如:

public static decimal EnsureThreeDecimalPlaces(decimal input) =>
    decimal.Add(input, 0.000m);

这似乎工作正常 - 如果您担心编译器将如何处理常量,您可以可以将这些位保存在一个数组中,只转换一次:

private static readonly decimal ZeroWithThreeDecimals =
    new decimal(new[] { 0, 0, 0, 196608 }); // 0.000m

public static decimal EnsureThreeDecimalPlaces(decimal input) =>
    decimal.Add(input, ZeroWithThreeDecimals);

我认为这有点过头了 - 特别是如果您有良好的单元测试。 (如果您针对将要部署的已编译代码进行测试,编译器之后将无法进入那里 - 如果看到 JIT 介入此处,我会 真的 感到惊讶。)

Decimal.ToString() 方法输出 小数位数 ,这是由结构的内部比例因子确定的。该系数的范围为 0 到 28。您可以通过调用 Decimal.GetBits Method. This method's name is slightly misleading as it returns an array of four integer values that can be passed to the Decimal Constructor (Int32[]); 获取确定该比例系数的信息;我提到这个构造函数的原因是文档的 "Remarks" 部分比 GetBits 方法的文档更好地描述了位布局。

使用此信息,您可以确定 Decimal 值的比例因子,从而知道默认 ToString 方法将产生多少小数位。以下代码将此演示为名为 "Scale" 的扩展方法。我还包括一个名为 "ToStringMinScale" 的扩展方法,用于将 Decimal 格式化为最小比例因子值。如果 Decimal 的比例因子大于指定的最小值,将使用该值。

internal static class DecimalExtensions
    {
    public static Int32 Scale(this decimal d)
        {
        Int32[] bits = decimal.GetBits(d);

        // From: Decimal Constructor (Int32[]) - Remarks
        // https://msdn.microsoft.com/en-us/library/t1de0ya1(v=vs.100).aspx

        // The binary representation of a Decimal number consists of a 1-bit sign, 
        // a 96-bit integer number, and a scaling factor used to divide 
        // the integer number and specify what portion of it is a decimal fraction. 
        // The scaling factor is implicitly the number 10, raised to an exponent ranging from 0 to 28.

        // bits is a four-element long array of 32-bit signed integers.

        // bits [0], bits [1], and bits [2] contain the low, middle, and high 32 bits of the 96-bit integer number.

        // bits [3] contains the scale factor and sign, and consists of following parts:

        // Bits 0 to 15, the lower word, are unused and must be zero.

        // Bits 16 to 23 must contain an exponent between 0 and 28, which indicates the power of 10 to divide the integer number.

        // Bits 24 to 30 are unused and must be zero.

        // Bit 31 contains the sign; 0 meaning positive, and 1 meaning negative.

        // mask off bits 0 to 15
        Int32 masked = bits[3] & 0xF0000;
        // shift masked value 16 bits to the left to obtain the scaleFactor
        Int32 scaleFactor = masked >> 16;

        return scaleFactor;
        }

    public static string ToStringMinScale(this decimal d, Int32 minScale)
        {
        if (minScale < 0 || minScale > 28)
            {
            throw new ArgumentException("minScale must range from 0 to 28 (inclusive)");
            }
        Int32 scale = Math.Max(d.Scale(), minScale);
        return d.ToString("N" + scale.ToString());
        }

    }