double型(浮動小数点数)の==比較に潜む罠
C#で数値を扱う際、double型(やfloat型)の値を == 演算子で直接比較すると、意図しない結果になることがあり、これは重大なバグの原因となります。
例えば、0.1 + 0.2 を計算すれば、当然 0.3 になるはずです。しかし、以下のコードを実行すると、期待に反して「等しくない」と出力されます。
using System;
public class DoubleComparisonProblem
{
public static void Main()
{
double val1 = 0.1;
double val2 = 0.2;
double sum = val1 + val2; // 0.3 になることを期待
double expected = 0.3;
// C#コンソールでは、sum の内部的な値は 0.30000000000000004 と表示されることがある
Console.WriteLine($"計算結果のsum: {sum:R}"); // R: 内部値を表示
Console.WriteLine($"期待値: {expected}");
// 比較 (==)
if (sum == expected)
{
Console.WriteLine("結果: 等しい");
}
else
{
Console.WriteLine("結果: 等しくない"); // こちらが実行される
}
}
}
出力結果(例):
計算結果のsum: 0.30000000000000004
期待値: 0.3
結果: 等しくない
この記事では、なぜこの問題が起きるのか、そしてdouble型を安全に比較するための正しい方法について解説します。
なぜ == での比較は失敗するのか?
この問題の原因は、double型が数値を**2進数の浮動小数点数(IEEE 754規格)**として格納している点にあります。
私たちが普段使う10進数では 0.1 や 0.2 はキリの良い数値ですが、これを2進数に変換しようとすると、1/3 が 0.333... となるように、無限循環小数になってしまいます。
double型は有限のメモリしか持たないため、0.1 は「0.1 に非常に近い近似値」として格納されます。その結果、0.1(の近似値)と 0.2(の近似値)を足した結果は、0.3(の近似値)とはわずかに異なる値(例: 0.30000000000000004)となり、== による厳密な比較は false となります。
解決策:許容誤差(イプシロン)を用いた比較
浮動小数点数を安全に比較するための標準的な方法は、「2つの数値の差の絶対値が、非常に小さい特定の許容誤差(イプシロン)未満であるか」をチェックすることです。
Math.Abs(a - b) < Epsilon
このEpsilon(許容誤差)には、1.0E-9(0.000000001)のような、計算の要求精度に応じた固定の微小な値を使用します。
コード例:安全な比較ヘルパーメソッド
このロジックを再利用可能なメソッドとして実装するのが一般的です。
using System;
public class DoubleComparisonSolution
{
// 2つのdouble値が「ほぼ等しい」か比較する
public static bool IsApproximatelyEqual(double valueA, double valueB)
{
// 許容誤差を定義(この値は要件に応じて調整する)
const double Epsilon = 1.0E-9;
// 2つの数値の差の絶対値が、許容誤差(Epsilon)より小さいかを判定
return Math.Abs(valueA - valueB) < Epsilon;
}
public static void Main()
{
double val1 = 0.1;
double val2 = 0.2;
double sum = val1 + val2;
double expected = 0.3;
// 危険な '==' 比較の代わりに、ヘルパーメソッドを使用
if (IsApproximatelyEqual(sum, expected))
{
Console.WriteLine("結果: ほぼ等しい"); // こちらが実行される
}
else
{
Console.WriteLine("結果: 等しくない");
}
}
}
出力結果:
結果: ほぼ等しい
注意点:double.Epsilon の誤用
double型には double.Epsilon という定数が存在しますが、これは前述の「許容誤差」として使用するべきではありません。
double.Epsilon(約 4.9E-324)は、「0(ゼロ)より大きい、double型で表現可能な最小の正の数」を意味します。これは計算誤差を比較するには小さすぎ、事実上 == と同じ結果になってしまいます。
補足:10進数の精度が必要な場合は decimal 型
もし、プログラムが金融計算や請求処理など、10進数(0.1や0.01)の精度を厳密に扱う必要がある場合、double型ではなく decimal 型を使用するべきです。
decimal型は数値を10進数(内部的には128ビット)として保持するため、0.1 のような10進数の小数を誤差なく格納できます。
using System;
public class DecimalExample
{
public static void Main()
{
// 'm' を付けると decimal リテラルになる
decimal val1 = 0.1m;
decimal val2 = 0.2m;
decimal sum = val1 + val2;
decimal expected = 0.3m;
// decimal 型同士の '==' 比較は安全に機能する
if (sum == expected)
{
Console.WriteLine("Decimalの結果: 等しい"); // こちらが実行される
}
else
{
Console.WriteLine("Decimalの結果: 等しくない");
}
}
}
出力結果:
Decimalの結果: 等しい
まとめ
double型およびfloat型は、2進数の近似値であるため、==による厳密な比較は避けるべきです。- 比較を行う際は、
Math.Abs(a - b) < Epsilonのように、小さな「許容誤差(Epsilon)」を設けて判定します。 - 金融計算など、10進数の小数を誤差なく厳密に扱いたい場合は、
double型ではなくdecimal型を使用します。
