連載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進で近い値を持つ
float と double は、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.2 が 0.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 は == で見ない
double や float は、ぴったり一致ではなく「この差なら同じとみなす」で判定します。
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 を増やさなければ広がりを止めやすくなります。
レビューで確認する項目
- 金額、税、単価、ポイントに
doubleやfloatが入っていないか -
doubleやfloatを==で比較していないか - 許容差の数字が場所ごとで増えていないか
-
Math.Roundに規則が書かれているか - 明細、合計、確定点のどこで丸めるか決まっているか
- cast が利用側へ広がっていないか
- 件数や ID を小数で持っていないか
まとめ
- 金額は
decimal - 通常の実数計算は
double -
floatは理由がある時だけ使う -
doubleとfloatは==で見ない -
decimalでは丸め規則と丸め位置まで決める
型の選び方だけで終わらず、比較方法と丸め位置まで決めると、後から追いやすいコードになります。
連載Index(読む順・公開済リンクが最新): S00_門前の誓い_総合Index