0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C#で float / double / decimal を迷わず選ぶ|金額はdecimal、通常はdouble【鍛錬K28】

0
Posted at

連載Index(読む順・公開済リンクが最新): S00_門前の誓い_総合Index

C# の float double decimal は、同じ小数でも使いどころが違います。

先に答えを書くと、選び方は次です。

  • 金額、税、単価、ポイントdecimal
  • 割合、距離、温度、座標、統計、一般的な実数計算double
  • 画像、音声、GPU、センサー、大きな配列float を検討する

迷いやすいのは、次の3つです。

  • 金額に double を使ってよいか
  • double== で比較してよいか
  • decimal を使えば金額のずれが必ず消えるのか

このページでは、最初に使い分けを表で整理し、そのあとで失敗例、既存コードの直し方、レビュー項目をまとめます。

前提環境

  • C# 12
  • .NET 8
  • 考え方自体は .NET Framework 4.8.1 でも同じ

まずはここだけ見る|使い分け表

用途 選ぶ型 比較方法 丸め 間違えた時に起きやすいこと
金額、税、単価、ポイント decimal 同じ桁数・同じ規則で比較 必要 明細と合計で1円ずれる
割合、距離、温度、統計、座標 double == を避けて許容差で比較 通常は表示時だけ 0.1 + 0.2、累積誤差
画像、音声、GPU、センサー float == を避けて許容差で比較 通常は表示時だけ 精度不足、境界で判定ぶれ

float / double / decimal の違い

float と double は 2進で近い値を持つ

floatdouble は、10進の 0.1 をそのまま持つ型ではありません。
2進で近い値を持つため、見た目どおりに一致しないことがあります。

double a = 0.1;
double b = 0.2;
double c = a + b;

Console.WriteLine(c);
Console.WriteLine(c == 0.3);

この比較が false になるのは、double が壊れているからではなく、近い値を持つ型だからです。

decimal は 10進小数の金額計算に向く

decimal は、金額や単価のような 10進小数に向いています。

decimal price = 10.25m;
decimal qty = 3m;
decimal total = price * qty;

Console.WriteLine(total);

ただし、金額では型だけでは足りません。
次も先に決めます。

  • 小数何桁まで扱うか
  • 端数処理をどうするか
  • 明細で丸めるか、合計で丸めるか

よくある失敗

金額で迷ったら decimal、通常の実数計算で迷ったら double です。
この前提で失敗例を見ると、どこで崩れるかを追いやすくなります。

0.1 + 0.20.3 と一致しない

double x = 0.1 + 0.2;
Console.WriteLine(x == 0.3);

double では、この比較は外れることがあります。
実数計算で == を使うと、条件分岐やテストが時々落ちます。

足し込み後に == 比較すると外れやすい

double total = 0.0;

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

Console.WriteLine(total);
Console.WriteLine(total == 1.0);

1回の計算では見えなくても、加算を繰り返すと差が見えやすくなります。

decimal でも丸め位置で結果が変わる

decimal unitPrice = 33.335m;
int qty1 = 3;
int qty2 = 3;

decimal line1 = Math.Round(unitPrice * qty1, 2, MidpointRounding.AwayFromZero);
decimal line2 = Math.Round(unitPrice * qty2, 2, MidpointRounding.AwayFromZero);

decimal byLine = line1 + line2;
decimal byTotal = Math.Round(unitPrice * (qty1 + qty2), 2, MidpointRounding.AwayFromZero);

Console.WriteLine(byLine);
Console.WriteLine(byTotal);

ここで結果が変わる理由は、型ではなく丸め位置です。
金額では decimal を選ぶだけではなく、どこで丸めるかまで決めます。

double と float は == で見ない

doublefloat は、ぴったり一致ではなく「この差なら同じとみなす」で判定します。

static bool NearlyEqual(double a, double b, double eps = 1e-12)
    => Math.Abs(a - b) <= eps;
double a = 0.1 + 0.2;
double b = 0.3;

Console.WriteLine(NearlyEqual(a, b));

値の大きさが大きく変わる場面では、相対差も使います。

static bool NearlyEqualRelative(double a, double b, double rel = 1e-12, double abs = 1e-12)
{
    var diff = Math.Abs(a - b);
    if (diff <= abs) return true;
    return diff <= Math.Max(Math.Abs(a), Math.Abs(b)) * rel;
}

許容差は、場所ごとに別の数字を書かずに1か所へまとめます。

static class Tolerance
{
    public const double General = 1e-12;
}
bool isSame = NearlyEqual(a, b, Tolerance.General);

金額で見るのは型だけではない

金額では、次の3点を先に決めます。

  • 小数何桁まで扱うか
  • 端数処理をどうするか
  • どこで丸めるか

端数処理はコードに出します。

decimal amount = 10.005m;
decimal rounded = Math.Round(amount, 2, MidpointRounding.AwayFromZero);

丸め処理は1か所へまとめます。

public static class Money
{
    public const int Scale = 2;

    public static decimal Round(decimal value)
        => Math.Round(value, Scale, MidpointRounding.AwayFromZero);
}
decimal subtotal = unitPrice * qty;
decimal total = Money.Round(subtotal);

既存コードで混在した時の直し方

既存コードでは、利用側で cast する形が増えやすくなります。

NG例

decimal total = 0m;

foreach (var row in rows)
{
    total += (decimal)(row.UnitPriceDouble * row.Qty);
}

この書き方だと、次が見えにくくなります。

  • どこで decimal に変えたか
  • どこで丸めたか
  • 同じ変換が他にあるか

OK例

public static class Money
{
    public const int Scale = 2;

    public static decimal Round(decimal value)
        => Math.Round(value, Scale, MidpointRounding.AwayFromZero);

    public static decimal FromDouble(double value)
        => Round((decimal)value);
}
decimal total = 0m;

foreach (var row in rows)
{
    total += Money.FromDouble(row.UnitPriceDouble * row.Qty);
}

変換と丸めを1か所へまとめると、利用側の式が短くなり、見直す場所も減ります。

直す順番

既存コードを見直す時は、次の順で進めると整理しやすくなります。

1. 値の意味で分ける

  • 金額、税、単価、ポイント
  • 割合、距離、時間、温度、座標
  • 件数、回数、ID

2. 境目を決める

  • API 入出力
  • DB 読み書き
  • 帳票出力
  • 画面表示
  • 外部連携

3. 金額から直す

  • 金額を decimal にする
  • 丸め規則を決める
  • 丸め位置を決める

4. 新規コードで cast を増やさない

既存部分をすぐ変えられなくても、新しく書く部分で cast を増やさなければ広がりを止めやすくなります。

レビューで確認する項目

  • 金額、税、単価、ポイントに doublefloat が入っていないか
  • doublefloat== で比較していないか
  • 許容差の数字が場所ごとで増えていないか
  • Math.Round に規則が書かれているか
  • 明細、合計、確定点のどこで丸めるか決まっているか
  • cast が利用側へ広がっていないか
  • 件数や ID を小数で持っていないか

まとめ

  • 金額は decimal
  • 通常の実数計算は double
  • float は理由がある時だけ使う
  • doublefloat== で見ない
  • decimal では丸め規則と丸め位置まで決める

型の選び方だけで終わらず、比較方法と丸め位置まで決めると、後から追いやすいコードになります。

連載Index(読む順・公開済リンクが最新): S00_門前の誓い_総合Index

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?