Почему .NET decimal.ToString (string) округляется от нуля, что явно не согласуется со спецификацией языка? - PullRequest
18 голосов
/ 12 февраля 2010

Я вижу, что в C # для округления decimal по умолчанию используется MidpointRounding.ToEven. Это ожидается, и это то, что диктует спецификация C #. Однако, учитывая следующее:

  • A decimal dVal
  • Формат string sFmt, который при передаче в dVal.ToString(sFmt) приведет к строке, содержащей округленную версию dVal

... очевидно, что decimal.ToString(string) возвращает значение, округленное с использованием MidpointRounding.AwayFromZero. Это может показаться прямым противоречием спецификации C #.

У меня такой вопрос: Есть ли веская причина, почему это так? Или это просто несоответствие в языке?

Ниже для справки я включил некоторый код, который записывает в консоль набор результатов операции округления и результатов операции decimal.ToString(string), каждый на каждое значение в массиве decimal значений. Фактические результаты встроены. После этого я включил соответствующий параграф из раздела «Спецификация языка C #» для типа 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 ]

6 голосов
/ 12 февраля 2010

Если вы внимательно прочитаете спецификацию, вы увидите, что здесь нет несоответствий.

Вот снова этот абзац с выделенными важными частями:

Результатом операции над значениями типа 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 цифр), округление банкира используется для определения того, каким будет результат.

1 голос
/ 12 февраля 2010

Скорее всего, потому что это стандартный способ обращения с валютой. Стимулом для создания десятичной дроби было то, что с плавающей точкой плохо справляется с валютными значениями, поэтому можно ожидать, что ее правила будут в большей степени соответствовать стандартам бухгалтерского учета, чем математической корректности.

1 голос
/ 12 февраля 2010

ToString() по умолчанию форматирует в соответствии с Culture, а не в соответствии с вычислительным аспектом спецификации. По-видимому, Culture для вашей локали (и большинство, судя по всему) ожидает округления от нуля.

Если вам нужно другое поведение, вы можете передать IFormatProvider в ToString()

Я думал выше, но вы правы, что оно всегда округляется от нуля, независимо от Culture. Проверьте ссылки в комментариях для доказательства.

...