閉じる

C#の浮動小数点数比較における誤差対策:等価判定で失敗しないための実装テクニック

C#を使用して数値計算やロジックの実装を行う際、多くのエンジニアが一度は直面するのが「浮動小数点数の比較」に関する問題です。

一見すると単純な数値比較に見えますが、コンピュータが内部で小数を扱う仕組みを正しく理解していないと、意図しないバグを引き起こす原因となります。特に、等価演算子(==)を用いた単純な比較は、浮動小数点数においては「ほぼ確実に失敗する」と言っても過言ではありません。

本記事では、2026年現在の最新の.NET環境を踏まえ、浮動小数点数の誤差が発生するメカニズムと、実務で使える具体的な比較テクニックを詳しく解説します。

浮動小数点数に誤差が生じる理由

C#のfloat(単精度)やdouble(倍精度)は、IEEE 754という標準規格に基づいた「浮動小数点数」として実装されています。

この形式は、非常に広範囲の数値を効率的に扱える一方で、10進数の小数を2進数で正確に表現できないという本質的な制約を抱えています。

10進数と2進数のギャップ

私たちが日常的に使っている10進数では、0.10.2は非常にシンプルな数値です。

しかし、これらを2進数に変換しようとすると、無限小数になってしまいます。

たとえば、10進数の0.1を2進数で表すと、0.0001100110011...と循環し続けます。

コンピュータのメモリは有限であるため、この無限に続く数値をどこかで切り捨てなければなりません。

この「切り捨て」が発生した瞬間に、本来の値との間に微小な誤差(丸め誤差)が生まれます

演算の積み重ねによる誤差の増幅

単独の数値であれば無視できるほどの小さな誤差であっても、計算を繰り返すことでそのズレは拡大していきます。

以下のコードは、C#でよく知られている「誤差の罠」を示す典型的な例です。

C#
using System;

double value1 = 0.1 + 0.2;
double value2 = 0.3;

Console.WriteLine($"value1: {value1}");
Console.WriteLine($"value2: {value2}");
Console.WriteLine($"等価判定 (value1 == value2): {value1 == value2}");
実行結果
value1: 0.30000000000000004
value2: 0.3
等価判定 (value1 == value2): False

この結果からわかる通り、数学的には 0.1 + 0.2 = 0.3 ですが、プログラム上では 0.30000000000000004 となり、0.3 とは一致しません。

このような微小な差異が、条件分岐(if文)の結果を狂わせるのです。

等価判定で失敗しないための基本戦略

浮動小数点数を比較する場合、== 演算子を使うのではなく、「2つの値の差が十分に小さいか」を判定する方法を採るのが一般的です。

この「十分に小さい」とされる値を「エプシロン(Epsilon)」や「許容誤差(Tolerance)」と呼びます。

絶対誤差による比較

最もシンプルな方法は、2つの値の差の絶対値を求め、それが指定した閾値(エプシロン)未満であるかを確認することです。

C#
public static bool NearlyEqual(double a, double b, double epsilon)
{
    // 二つの値の差の絶対値が、許容誤差(epsilon)より小さければ等しいとみなす
    return Math.Abs(a - b) < epsilon;
}

この手法は、扱う数値の範囲があらかじめ分かっている場合に有効です。

しかし、double が扱う数値が非常に大きい場合や、逆に非常に小さい場合には、適切なエプシロンの値を決めるのが難しくなります。

相対誤差による比較

数値の大きさに応じて許容誤差を調整するのが「相対誤差」を用いた比較です。

非常に大きな値同士を比較する場合、絶対的な差が 0.00001 であっても、それは全体のスケールから見れば無視できるほど小さいかもしれません。

逆に、非常に小さな値同士では、その差が致命的になることもあります。

C#
public static bool RelativeNearlyEqual(double a, double b, double epsilon)
{
    if (a == b) return true; // 全く同じなら早期リターン

    double diff = Math.Abs(a - b);
    double absA = Math.Abs(a);
    double absB = Math.Abs(b);
    
    // a または b の大きい方に対する相対的な誤差で判定する
    return diff / Math.Max(absA, absB) < epsilon;
}

このように、データの性質に合わせて誤差の定義を使い分けることが、堅牢なプログラムを書くための第一歩です。

C#における特殊な数値の取り扱い

比較を行う際には、通常の数値だけでなく、NaN(Not a Number)や Infinity(無限大)といった特殊な値にも注意を払う必要があります。

double.Epsilon の誤解

C#の double 型には定数 double.Epsilon が定義されています。

しかし、これをそのまま等価判定の閾値として使うことは推奨されません。

なぜなら、double.Epsilon は「0より大きい、表現可能な最小の正の値」を指しており、一般的な計算結果の誤差よりもはるかに小さい値だからです。

ほとんどの計算誤差はこの値よりも大きくなるため、Math.Abs(a - b) < double.Epsilon と書いても、期待通りに「等しい」と判定されることは稀です。

NaN の比較

浮動小数点数の世界では、NaN はいかなる値とも等しくないと定義されています。

自分自身と比較しても false になります。

C#
double myNan = double.NaN;
Console.WriteLine(myNan == double.NaN); // False と出力される
Console.WriteLine(double.IsNaN(myNan)); // True と出力される(正しい判定方法)

比較ロジックを自作する場合は、引数に NaN が含まれる可能性を考慮し、double.IsNaN() メソッドを活用することが重要です。

実践的な実装例:汎用的な比較メソッド

ここでは、実務のプロジェクトですぐに利用できる、絶対誤差と相対誤差を組み合わせた堅牢な比較メソッドの例を紹介します。

C#
public static class FloatExtensions
{
    // デフォルトの許容誤差(プロジェクトの要件に応じて調整)
    private const double DefaultEpsilon = 1e-10;

    public static bool IsAlmostEqualTo(this double value1, double value2, double epsilon = DefaultEpsilon)
    {
        // 1. 全く同じ値(または両方とも無限大)の場合
        if (value1 == value2)
        {
            return true;
        }

        // 2. NaNが含まれている場合の処理
        if (double.IsNaN(value1) || double.IsNaN(value2))
        {
            return false;
        }

        // 3. 絶対誤差の確認
        double diff = Math.Abs(value1 - value2);
        if (diff < epsilon)
        {
            return true;
        }

        // 4. 相対誤差の確認(非常に大きい数値同士の比較用)
        value1 = Math.Abs(value1);
        value2 = Math.Abs(value2);
        double largest = (value2 > value1) ? value2 : value1;

        return diff <= largest * epsilon;
    }
}

このメソッドは拡張メソッドとして定義されているため、以下のように直感的に記述できます。

C#
double result = 0.1 + 0.2;
if (result.IsAlmostEqualTo(0.3))
{
    Console.WriteLine("ほぼ等しいと判定されました");
}

誤差を回避するための根本的な対策:decimal型の活用

「そもそも誤差が発生しては困る」というケース、特に金融計算や会計処理においては、floatdouble を使用すべきではありません。

代わりに C# が提供する decimal 型を使用します。

decimal 型の特徴

decimal は128ビットのデータ型であり、内部的には「10進数」として数値を保持します。

そのため、0.1 のような値を正確に表現でき、== 演算子での比較も期待通りに動作します。

特徴float / doubledecimal
表現形式2進浮動小数点数10進浮動小数点数
精度低 ~ 中非常に高い (28~29桁)
速度非常に高速 (CPU支援)低速 (ソフトウェア処理)
主な用途科学計算、物理シミュレーション、グラフィックス金融計算、税計算、通貨の扱い

decimal による比較の例

C#
decimal d1 = 0.1m + 0.2m;
decimal d2 = 0.3m;

Console.WriteLine($"d1: {d1}");
Console.WriteLine($"d2: {d2}");
Console.WriteLine($"等価判定: {d1 == d2}");
実行結果
d1: 0.3
d2: 0.3
等価判定: True

decimal を使えば、浮動小数点数特有の「小さなズレ」に悩まされることはありません。

ただし、double に比べて計算速度が遅く、メモリ消費も大きいため、すべての数値を decimal にするのではなく、用途に応じて使い分けるのがプロの設計です。

.NET 9/10 (2026年最新環境) における進化

2026年現在の .NET 環境では、ジェネリック数学(Generic Math)の普及により、数値型の比較はより抽象化され、安全に行えるようになっています。

IFloatingPointIeee754 インターフェース

最新の .NET では、floatdoubleIFloatingPointIeee754<T> インターフェースを実装しています。

これにより、特定の型に依存しない汎用的な誤差比較メソッドを記述することが容易になりました。

C#
using System.Numerics;

public static bool GenericNearlyEqual<T>(T a, T b, T epsilon) 
    where T : IFloatingPointIeee754<T>
{
    return T.Abs(a - b) < epsilon;
}

このようなジェネリックな実装を行うことで、Half(半精度)、floatdouble といった異なる精度を持つ浮動小数点数に対して、同一のロジックで安全な比較を提供できるようになります。

ユニットテストでの比較

実務においては、ロジックの正しさを保証するためにユニットテストで浮動小数点数を比較する場面が多くあります。

この際、テストフレームワークが提供する専用の比較機能を使うのがベストプラクティスです。

NUnit や xUnit での比較

多くのテストライブラリには、あらかじめ許容誤差を指定できるアサーションが用意されています。

  • NUnitの場合:
    Assert.That(actual, Is.EqualTo(0.3).Within(0.00001));

  • xUnitの場合:
    Assert.Equal(0.3, actual, precision: 5);

自分で Math.Abs を書くよりも、テストフレームワークの機能を活用したほうが、テスト失敗時のエラーメッセージが分かりやすくなるというメリットがあります。

まとめ

C#における浮動小数点数の比較は、一見単純に見えて非常に奥が深いテーマです。

等価判定で失敗しないためのポイントを改めて整理しましょう。

  1. 原理を理解するfloatdouble は2進数であるため、10進数の計算では必ず微小な誤差が生じます。
  2. 直接比較を避ける== 演算子による比較はバグの温床です。必ず許容誤差(Epsilon)を考慮した比較メソッドを使用してください。
  3. 適切な型を選択する:誤差が許されない金融計算などでは、パフォーマンスを犠牲にしても decimal 型を採用すべきです。
  4. 最新の機能を活用する:.NET のジェネリック数学や標準的な比較インターフェースを活用し、再利用性の高いコードを目指しましょう。

浮動小数点数は、コンピュータが現実世界の複雑な数値を扱うための強力な道具です。

その特性を正しく理解し、適切な比較テクニックをマスターすることで、「計算結果が微妙に合わない」というストレスから解放され、より信頼性の高いアプリケーションを構築できるようになります。

今後の開発において、本記事で紹介したテクニックをぜひ役立ててください。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!