Почему.NET decimal.ToString(string) округляется от нуля, что явно не согласуется со спецификацией языка?
Я вижу, что в C#, округляя decimal
по умолчанию использует MidpointRounding.ToEven
, Это ожидается, и это то, что диктует спецификация C#. Однако, учитывая следующее:
decimal dVal
- Формат
string sFmt
что, когда передается вdVal.ToString(sFmt)
, приведет к строке, содержащей округленную версиюdVal
... очевидно, что decimal.ToString(string)
возвращает значение, округленное с помощью MidpointRounding.AwayFromZero
, Это может показаться прямым противоречием спецификации C#.
У меня такой вопрос: есть ли веская причина, почему это так? Или это просто несоответствие в языке?
Ниже для справки я включил некоторый код, который записывает в консоль целый ряд результатов операции округления и decimal.ToString(string)
результаты операции, каждое на каждое значение в массиве decimal
ценности. Фактические результаты встроены. После этого я включил соответствующий абзац из раздела C# Language Specification на decimal
тип.
Пример кода:
static void Main(string[] args)
{
decimal[] dArr = new decimal[] { 12.345m, 12.355m };
OutputBaseValues(dArr);
// Base values:
// d[0] = 12.345
// d[1] = 12.355
OutputRoundedValues(dArr);
// Rounding with default MidpointRounding:
// Math.Round(12.345, 2) => 12.34
// Math.Round(12.355, 2) => 12.36
// decimal.Round(12.345, 2) => 12.34
// decimal.Round(12.355, 2) => 12.36
OutputRoundedValues(dArr, MidpointRounding.ToEven);
// Rounding with mr = MidpointRounding.ToEven:
// Math.Round(12.345, 2, mr) => 12.34
// Math.Round(12.355, 2, mr) => 12.36
// decimal.Round(12.345, 2, mr) => 12.34
// decimal.Round(12.355, 2, mr) => 12.36
OutputRoundedValues(dArr, MidpointRounding.AwayFromZero);
// Rounding with mr = MidpointRounding.AwayFromZero:
// Math.Round(12.345, 2, mr) => 12.35
// Math.Round(12.355, 2, mr) => 12.36
// decimal.Round(12.345, 2, mr) => 12.35
// decimal.Round(12.355, 2, mr) => 12.36
OutputToStringFormatted(dArr, "N2");
// decimal.ToString("N2"):
// 12.345.ToString("N2") => 12.35
// 12.355.ToString("N2") => 12.36
OutputToStringFormatted(dArr, "F2");
// decimal.ToString("F2"):
// 12.345.ToString("F2") => 12.35
// 12.355.ToString("F2") => 12.36
OutputToStringFormatted(dArr, "###.##");
// decimal.ToString("###.##"):
// 12.345.ToString("###.##") => 12.35
// 12.355.ToString("###.##") => 12.36
Console.ReadKey();
}
private static void OutputBaseValues(decimal[] dArr)
{
Console.WriteLine("Base values:");
for (int i = 0; i < dArr.Length; i++) Console.WriteLine("d[{0}] = {1}", i, dArr[i]);
Console.WriteLine();
}
private static void OutputRoundedValues(decimal[] dArr)
{
Console.WriteLine("Rounding with default MidpointRounding:");
foreach (decimal d in dArr) Console.WriteLine("Math.Round({0}, 2) => {1}", d, Math.Round(d, 2));
foreach (decimal d in dArr) Console.WriteLine("decimal.Round({0}, 2) => {1}", d, decimal.Round(d, 2));
Console.WriteLine();
}
private static void OutputRoundedValues(decimal[] dArr, MidpointRounding mr)
{
Console.WriteLine("Rounding with mr = MidpointRounding.{0}:", mr);
foreach (decimal d in dArr) Console.WriteLine("Math.Round({0}, 2, mr) => {1}", d, Math.Round(d, 2, mr));
foreach (decimal d in dArr) Console.WriteLine("decimal.Round({0}, 2, mr) => {1}", d, decimal.Round(d, 2, mr));
Console.WriteLine();
}
private static void OutputToStringFormatted(decimal[] dArr, string format)
{
Console.WriteLine("decimal.ToString(\"{0}\"):", format);
foreach (decimal d in dArr) Console.WriteLine("{0}.ToString(\"{1}\") => {2}", d, format, d.ToString(format));
Console.WriteLine();
}
Абзац из раздела 4.1.7 Спецификации языка C# ("Десятичный тип") (получить полную спецификацию здесь (.doc)):
Результатом операции со значениями типа decimal является результат вычисления точного результата (сохранение масштаба, определенного для каждого оператора), а затем округление до соответствия представлению. Результаты округляются до ближайшего представимого значения и, когда результат одинаково близок к двум представимым значениям, до значения, имеющего четное число в позиции наименее значащей цифры (это называется "округлением банкира"). Нулевой результат всегда имеет знак 0 и масштаб 0.
Легко видеть, что они, возможно, не рассматривали ToString(string)
в этом пункте, но я склонен думать, что он вписывается в это описание.
3 ответа
ToString()
по умолчанию форматирует в соответствии с Culture
не в соответствии с вычислительным аспектом спецификации. Видимо Culture
для вашей локали (и большинства, судя по всему) ожидает округления от нуля.
Если вы хотите другое поведение, вы можете передать IFormatProvider
в к ToString()
Я думал выше, но вы правы, что он всегда округляется от нуля, независимо от того, Culture
, Проверьте ссылки в комментариях для доказательства.
Если вы внимательно прочитаете спецификацию, вы увидите, что здесь нет противоречий.
Вот этот пункт снова, с выделенными важными частями:
Результатом операции со значениями типа decimal является результат вычисления точного результата (с сохранением масштаба, определенного для каждого оператора), а затем округления для соответствия представлению. Результаты округляются до ближайшего представимого значения и, когда результат одинаково близок к двум представимым значениям, до значения, имеющего четное число в позиции наименее значащей цифры (это называется "округлением банкира"). Нулевой результат всегда имеет знак 0 и масштаб 0.
Эта часть спецификации относится к арифметическим операциям на decimal
; Форматирование строк не является одним из них, и даже если бы это было так, это не имело бы значения, потому что ваши примеры имеют низкую точность.
Чтобы продемонстрировать поведение, указанное в спецификации, используйте следующий код:
Decimal d1 = 0.00000000000000000000000000090m;
Decimal d2 = 0.00000000000000000000000000110m;
// Prints: 0.0000000000000000000000000004 (rounds down)
Console.WriteLine(d1 / 2);
// Prints: 0.0000000000000000000000000006 (rounds up)
Console.WriteLine(d2 / 2);
Это все, о чем идет речь в спецификации. Если результат какого-либо вычисления превысит предел точности decimal
тип (29 цифр), округление банкира используется для определения результата.
Скорее всего потому, что это стандартный способ обращения с валютой. Стимулом для создания десятичной дроби было то, что с плавающей точкой плохо справляется с валютными значениями, поэтому можно ожидать, что ее правила будут в большей степени соответствовать стандартам бухгалтерского учета, чем математической корректности.