2014-07-25 51 views
2

Wikipedia says哪個C#雙重字面不能完全表示爲double?

例如,十進制數0.1未在任何有限精度的二進制 浮點

然而所能表述,在C#

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

寫入0.1

我會期待像0.099999999999999999左右。

我正在尋找至少一個不完全可表示爲double的示例雙字面值。

編輯:

正如其他人所指出的那樣,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)

+5

'0.1'僅僅是因爲系統在將其轉換爲字符串表示形式時欺騙了事物並不意味着底層表示*具有*精確地存儲了'0.1'。 –

+0

任何不是{1/2,1/4,1/8,1/16,...}的總和 – Exceptyon

+0

如果您還可以使用Java,則有一個非常有用的類java.util.BigDecimal 。它具有從雙精確轉換到精確轉換爲字符串。它可以完成那些結果爲有限長度小數的操作。我經常用它來分析浮點問題。例如'new BigDecimal(0.1).toString()'是「0.1000000000000000055511151231257827021181583404541015625」 –

回答

2

BCL作弊當您打印它時給予您一個四捨五入的值。應該沒有打印不同表示或準確性的文字。

這很好,因爲它大部分時間都符合直覺。但是到目前爲止,我還沒有找到一個很好的方法來獲得打印的值確切的

+4

你可以使用這個:http://pobox.com/~skeet/csharp/DoubleConverter.cs用作Console.WriteLine(DoubleConverter.ToExactString (d))' –

7

我有一個small source file它打印確切值存儲在double。答案末尾的代碼,以防鏈接消失。基本上,它獲取double的確切位,並從那裏開始。這不是漂亮或efficent,但它的工作原理:)

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

輸出:

0.1000000000000000055511151231257827021181583404541015625 

當你只需要使用0.1.ToString()的BCL截斷的文字表述爲您服務。

至於針對double values是完全表示 - 基本上,你需要制定出最接近的二進制表示是什麼,看到是否是精確值。基本上它需要在正確的範圍和精確度內由兩個冪(包括兩個負冪)組成。

例如,4。75能精確表示,因爲它是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); 
     } 
    } 
} 
+0

謝謝!那麼,爲什麼0.1.ToString(「N99」)對我說謊,即使我指定了高精度? –

+1

@Marco:文檔並不清楚,但我相信這是因爲有一個隱含的「返回最大數字」作爲您提供的值的上限,並且它截斷了尾部的0。 –

+1

更糟的是,我發現0.1m.Equals((十進制)0.1)。我認爲這很奇怪。你認爲這與你剛纔解釋過的字符串轉換有相同的根路線嗎? –

0

0.1是示例。但在C#中,它是double類型的文字。並且Double.ToString()默認使用精度爲15而不是17位的格式。從documentation

相關報價:

默認情況下,返回值只包含15位精度,儘管最大的17位內部維護。 [...]如果您需要更高精度,請指定具有「G17」格式規格的格式,該規格始終返回17位精度或「R」,如果可以用該精度或17位數表示該數字,則返回15位數字如果該數字只能以最高精度表示。

所以,0.1.ToString("G17")等於"0.10000000000000001"這是數0.1000000000000000055511151231257827021181583404541015625從喬恩斯基特回答到無窮正確舍入。請注意,第一個數字中的最後一個1是第17個有效數字,第二個數字中的第一個5是第十八個有效數字。 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頁面。