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

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

显然,我可以用"0.000######....#"进行格式化然后解析,但效率相对较低,而且我正在寻找避免转换为字符串/从字符串转换的解决方案。

我尝试过以下解决方案:

 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

UPDATE

继Jon Skeet的回答后,我之前尝试过添加0.000M ,但这对dotnetfiddle无效。 所以我很惊讶地看到Decimal.Add(d, 0.000M)确实有效。 这是一个dotnetfiddle比较d + 000M decimal.Add(d,0.000M)decimal.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); 

我认为这有点超过顶部 – 特别是如果你有好的unit testing。 (如果你对你将要部署的已编译代码进行测试,那么之后编译器就无法进入 – 我会非常惊讶地看到JIT介入此处。)

Decimal.ToString()方法输出根据结构的内部缩放因子确定的小数位数 。 此因子的范围为0到28.您可以通过调用Decimal.GetBits方法获取信息以确定此缩放因子。 这个方法的名称有点误导,因为它返回一个包含四个整数值的数组,可以传递给Decimal Constructor(Int32 []) ; 我提到这个构造函数的原因是文档的“备注”部分描述了比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()); } }