哪个C#double literal不能完全表示为double?

时间:2014-07-25 12:17:10

标签: c# double literals

Wikipedia says

  

例如,十进制数0.1不能用二进制表示   任何有限精度的浮点

然而,在C#

string s = 0.1.ToString(CultureInfo.InvariantCulture);
Console.WriteLine(s);

0.1

我原本期望0.099999999999999999左右。

我正在寻找至少一个双精简版的例子,它不能完全代表双重。

修改

正如其他人所指出的,0.1确实是我一直在寻找的文字,正如下面的经典代码示例所示:

double sum = 0.0;

for (int i = 0; i < 10; i++)
{
    sum += 0.1;
}

Console.WriteLine(sum.Equals(1.0)); // false

当双精度转换为其他数据类型时,会发生奇怪的事情。这不仅仅是string的情况,因为这个表达式是真的:0.1m.Equals((decimal)0.1)

3 个答案:

答案 0 :(得分:7)

我有small source file打印存储在double中的完全值。答案结束时的代码,以防链接消失。基本上,它取出double的确切位,并从那里开始。它不漂亮或不高效,但它有效:)

string s = DoubleConverter.ToExactString(0.1);
Console.WriteLine(s);

输出:

0.1000000000000000055511151231257827021181583404541015625

当您使用0.1.ToString()时,BCL会截断您的文字表示。

至于哪些double值可以准确表示 - 基本上,您需要计算出最接近的二进制表示形式,并查看的确切值。基本上它需要在正确的范围和精度范围内由2的幂(包括2的负幂)组成。

例如,4.75可以准确表示,因为它是2 2 + 2 -1 + 2 -2

源代码:

using System;
using System.Globalization;

/// <summary>
/// A class to allow the conversion of doubles to string representations of
/// their exact decimal values. The implementation aims for readability over
/// efficiency.
/// </summary>
public class DoubleConverter
{    
    /// <summary>
    /// Converts the given double to a string representation of its
    /// exact decimal value.
    /// </summary>
    /// <param name="d">The double to convert.</param>
    /// <returns>A string representation of the double's exact decimal value.</return>
    public static string ToExactString (double d)
    {
        if (double.IsPositiveInfinity(d))
            return "+Infinity";
        if (double.IsNegativeInfinity(d))
            return "-Infinity";
        if (double.IsNaN(d))
            return "NaN";

        // Translate the double into sign, exponent and mantissa.
        long bits = BitConverter.DoubleToInt64Bits(d);
        // Note that the shift is sign-extended, hence the test against -1 not 1
        bool negative = (bits < 0);
        int exponent = (int) ((bits >> 52) & 0x7ffL);
        long mantissa = bits & 0xfffffffffffffL;

        // Subnormal numbers; exponent is effectively one higher,
        // but there's no extra normalisation bit in the mantissa
        if (exponent==0)
        {
            exponent++;
        }
        // Normal numbers; leave exponent as it is but add extra
        // bit to the front of the mantissa
        else
        {
            mantissa = mantissa | (1L<<52);
        }

        // Bias the exponent. It's actually biased by 1023, but we're
        // treating the mantissa as m.0 rather than 0.m, so we need
        // to subtract another 52 from it.
        exponent -= 1075;

        if (mantissa == 0) 
        {
            return "0";
        }

        /* Normalize */
        while((mantissa & 1) == 0) 
        {    /*  i.e., Mantissa is even */
            mantissa >>= 1;
            exponent++;
        }

        /// Construct a new decimal expansion with the mantissa
        ArbitraryDecimal ad = new ArbitraryDecimal (mantissa);

        // If the exponent is less than 0, we need to repeatedly
        // divide by 2 - which is the equivalent of multiplying
        // by 5 and dividing by 10.
        if (exponent < 0) 
        {
            for (int i=0; i < -exponent; i++)
                ad.MultiplyBy(5);
            ad.Shift(-exponent);
        } 
        // Otherwise, we need to repeatedly multiply by 2
        else
        {
            for (int i=0; i < exponent; i++)
                ad.MultiplyBy(2);
        }

        // Finally, return the string with an appropriate sign
        if (negative)
            return "-"+ad.ToString();
        else
            return ad.ToString();
    }

    /// <summary>Private class used for manipulating
    class ArbitraryDecimal
    {
        /// <summary>Digits in the decimal expansion, one byte per digit
        byte[] digits;
        /// <summary> 
        /// How many digits are *after* the decimal point
        /// </summary>
        int decimalPoint=0;

        /// <summary> 
        /// Constructs an arbitrary decimal expansion from the given long.
        /// The long must not be negative.
        /// </summary>
        internal ArbitraryDecimal (long x)
        {
            string tmp = x.ToString(CultureInfo.InvariantCulture);
            digits = new byte[tmp.Length];
            for (int i=0; i < tmp.Length; i++)
                digits[i] = (byte) (tmp[i]-'0');
            Normalize();
        }

        /// <summary>
        /// Multiplies the current expansion by the given amount, which should
        /// only be 2 or 5.
        /// </summary>
        internal void MultiplyBy(int amount)
        {
            byte[] result = new byte[digits.Length+1];
            for (int i=digits.Length-1; i >= 0; i--)
            {
                int resultDigit = digits[i]*amount+result[i+1];
                result[i]=(byte)(resultDigit/10);
                result[i+1]=(byte)(resultDigit%10);
            }
            if (result[0] != 0)
            {
                digits=result;
            }
            else
            {
                Array.Copy (result, 1, digits, 0, digits.Length);
            }
            Normalize();
        }

        /// <summary>
        /// Shifts the decimal point; a negative value makes
        /// the decimal expansion bigger (as fewer digits come after the
        /// decimal place) and a positive value makes the decimal
        /// expansion smaller.
        /// </summary>
        internal void Shift (int amount)
        {
            decimalPoint += amount;
        }

        /// <summary>
        /// Removes leading/trailing zeroes from the expansion.
        /// </summary>
        internal void Normalize()
        {
            int first;
            for (first=0; first < digits.Length; first++)
                if (digits[first]!=0)
                    break;
            int last;
            for (last=digits.Length-1; last >= 0; last--)
                if (digits[last]!=0)
                    break;

            if (first==0 && last==digits.Length-1)
                return;

            byte[] tmp = new byte[last-first+1];
            for (int i=0; i < tmp.Length; i++)
                tmp[i]=digits[i+first];

            decimalPoint -= digits.Length-(last+1);
            digits=tmp;
        }

        /// <summary>
        /// Converts the value to a proper decimal string representation.
        /// </summary>
        public override String ToString()
        {
            char[] digitString = new char[digits.Length];            
            for (int i=0; i < digits.Length; i++)
                digitString[i] = (char)(digits[i]+'0');

            // Simplest case - nothing after the decimal point,
            // and last real digit is non-zero, eg value=35
            if (decimalPoint==0)
            {
                return new string (digitString);
            }

            // Fairly simple case - nothing after the decimal
            // point, but some 0s to add, eg value=350
            if (decimalPoint < 0)
            {
                return new string (digitString)+
                       new string ('0', -decimalPoint);
            }

            // Nothing before the decimal point, eg 0.035
            if (decimalPoint >= digitString.Length)
            {
                return "0."+
                    new string ('0',(decimalPoint-digitString.Length))+
                    new string (digitString);
            }

            // Most complicated case - part of the string comes
            // before the decimal point, part comes after it,
            // eg 3.5
            return new string (digitString, 0, 
                               digitString.Length-decimalPoint)+
                "."+
                new string (digitString,
                            digitString.Length-decimalPoint, 
                            decimalPoint);
        }
    }
}

答案 1 :(得分:2)

BCL在打印时给你一个舍入值。应该没有可以打印不同表示或准确性的文字。

这很好,因为它大多数时候都与直觉相符。但到目前为止,我还没有找到一种很好的方法来获取完全值来打印。

答案 2 :(得分:0)

0.1就是一个例子。但在C#中,它是double类型的文字。 Double.ToString()默认使用精度为15而不是17位的格式。

documentation的相关引用:

  

默认情况下,返回值仅包含15位精度,但内部最多保留17位数。 [...]如果您需要更高的精度,请使用&#34; G17&#34;指定格式。格式规范,总是返回17位精度,或者#34; R&#34;如果数字可以用该精度表示则返回15位数,如果数字只能以最大精度表示,则返回17位数。

因此,0.1.ToString("G17")等于"0.10000000000000001",这是来自Jon Skeet的数字0.1000000000000000055511151231257827021181583404541015625正确舍入到无穷大。请注意,第一个数字中的最后1是第17个有效数字,第二个数字中的第一个5是第18个有效数字。 0.1.ToString()0.1.ToString("G")基本相同,等于"0.1"。如果您在小数点后打印15位数,或"0.100000000000000"。这是"0.10000000000000001"正确舍入为零。

有趣的是,Convert.ToDecimal(double)仅使用了15位有效数字。

documentation的相关引用:

  

此方法返回的十进制值最多包含15位有效数字。如果value参数包含超过15位有效数字,则使用舍入舍入为最接近的数字。

您可以使用相同的G17格式和decimal.Parse()0.1转换为十进制:decimal.Parse(0.1.ToString("G17"))。此代码段产生的数字不等于0.1m

有关详细信息,请查看MSDN上的The General ("G") Format Specifier页。