連載Index(読む順・公開済(リンク)はここが最新): S00_門前の誓い_総合Index
「設定や計算結果を小数で持ちたい」「金額も割合も全部“数値”だ」
そう思って書き始めた結果、float / double / decimal が同じシステムに混ざります。
- 古い画面は
doubleのまま - 新しい要件で金額が出て
decimalが入る - 外部APIは
floatを返してくる
そして、計算や集計の途中で、だいたいこうなります。
「型が合わない?じゃあキャストで合わせる」
このページは float / double / decimal を用途で選ぶ基準 と、
すでに混在している既存コードで キャスト増殖を止める手順 をまとめます。
1. このページで押さえること
- float / double / decimal を「用途」で決める判断基準
- double/floatの比較は
==に寄せない(許容誤差で判定する) - decimalは丸め規則と丸め位置が本体(決めないと合わない)
- 既存が混在している時は「全面改修」より「増殖を止める」が優先
2. 結論(まずはこれ)
2-1. 3行で決める
-
金額・会計・請求・税・ポイント ->
decimal -
計測値・物理量・割合・統計・グラフ・一般的な実数 ->
double(基本) -
大量データで帯域/メモリが支配的 ->
float(理由がある時だけ)
迷ったら double。ただし金額は例外で decimal。
2-2. 早見表
| 用途 | 推奨 | こういう場面 | 注意点 |
|---|---|---|---|
| 金額/税/単価/ポイント | decimal | 請求、帳票、明細・合計、端数 | 丸め規則と丸め位置を決めないと合わない |
| 一般的な小数計算 | double | 計測、割合、統計、UI表示 | 近似誤差は必ず出る。比較は許容誤差 |
| 大量データ | float | 画像/音声/センサー/GPU | 精度が落ちる。理由がないなら選ばない |
3. そもそも何が違うのか(最短で)
3-1. float / double は「2進の近似」
float/doubleは 二進浮動小数点(IEEE 754) です。
0.1 のような10進小数は二進で有限桁に収まらないことが多く、近い値になります。
つまり「ズレるのが普通」です。
3-2. decimal は「10進で金額向き」
decimalは 10進 を扱いやすい設計なので、金額や単価に向きます。
ただし万能ではなく、重要なのは 丸め規則と丸め位置を設計として決めること です。
4. “キャストで合わせる” が増え始めると何が起きるか
混在現場でよく見るのは、このコードです。
// どこかはdouble、どこかはdecimal。合わないので、とりあえずキャスト
decimal total = 0m;
foreach (var row in rows)
{
total += (decimal)(row.UnitPriceDouble * row.Qty);
}
「とりあえず動く」けれど、ここには問題が残ります。
-
doubleの近似誤差をdecimalに移しているだけ(誤差が消えたわけではない) - どこで丸めるかが不明(明細/行/合計/確定点のどれなのか)
- キャストが散り始めると、追跡できなくなる
このページの後半は、ここを止めるための手順です。
まずは “ズレ方” を例で押さえます。
5. よく出るズレ(例で押さえる)
5-1. 0.1 + 0.2 が 0.3 にならない
double a = 0.1;
double b = 0.2;
double c = a + b;
Console.WriteLine(c);
Console.WriteLine(c == 0.3); // true にならないことがある
doubleは「0.3を正確に表す」ではなく「近い値を持つ」ので、等価比較が崩れます。
5-2. 足し込みで 1.0 にならない
double x = 0.0;
for (int i = 0; i < 10; i++) x += 0.1;
Console.WriteLine(x);
Console.WriteLine(x == 1.0); // ここに依存すると不安定になる
5-3. “金額っぽい値” を double で扱うと、帳尻が合いにくくなる
double unit = 10.01;
int qty = 3;
double total = unit * qty;
Console.WriteLine(total);
表示で丸めれば一旦それっぽく見えます。
ただ、明細・合計・税計算・比較が混ざると、説明が苦しくなります。
金額は最初からdecimalで揃えた方が筋が通ります。
6. double/floatの比較は「許容誤差」で決める
結論: == に寄せない。境界は揺れる前提で設計します。
6-1. 絶対誤差(値のスケールが一定の時)
static bool NearlyEqual(double a, double b, double eps = 1e-12)
=> Math.Abs(a - b) <= eps;
6-2. 相対誤差(値のスケールが大きく振れる時)
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;
}
6-3. eps(許容誤差)を散らさない
- 画面は
1e-9、バッチは1e-12のように散ると、境界の挙動が割れます - epsは仕様に近い値です。置き場所を決めます(ドメイン/設定/境界)
7. decimalで詰まりやすいのは「丸め」
decimalは金額向きですが、放置すると「合わない」が起きます。
原因はたいてい 丸め規則と丸め位置が揃っていない ことです。
7-1. 最初に決める3点(仕様)
- 小数何桁まで扱うか(例: 2桁、3桁)
- 端数処理(四捨五入/切り捨て/切り上げ/銀行丸め など)
- どこで丸めるか(明細/行/合計/確定点のどこか)
7-2. Math.Roundは丸め規則を明示する
decimal price = 10.005m;
decimal rounded = Math.Round(price, 2, MidpointRounding.AwayFromZero);
「明示しない」が一番危ないです。期待と違った時に説明できません。
7-3. 丸め処理は境界に寄せる
- 途中計算はなるべく高い精度で持つ
- 丸めは「境界」で責任を持つ(表示/永続化/請求確定など)
丸めが散ると、追跡が難しくなります。
8. floatを選ぶ理由(必要な時だけ)
floatを選ぶのは「軽そうだから」ではなく、理由がある時だけです。
- 大量データでメモリ帯域が支配的
- GPU/画像/音声/センサーなど float 前提の世界
- 精度より速度が明確に優先される
業務UIや金額で「軽そうだからfloat」は、後で修正が重くなります。
9. 既存コードが混在している時の対処(現場の止血)
混在現場で一番きついのは、「型が合わない」ではありません。
キャストが点在し始めて、ズレの責任が追えなくなる ことです。
ここでやることは「全部直す」ではなく、次の順です。
9-1. まず“数値の意味”を分類する
同じ小数でも意味が違います。
- 金額/単価/税/ポイント -> decimal候補
- 計測値/距離/時間/割合 -> double候補
- 件数/回数/ID -> int/long候補(小数にしない)
分類できると、変換の方向が決まります。
9-2. “触る範囲”を決める(無理に全部直さない)
- 影響範囲が小さい(境界だけ) -> 変える価値がある
- 影響範囲が大きい(深部まで波及) -> まずは止血(境界で吸収)に寄せる
9-3. キャスト増殖を止める(境界に集約する)
やることは単純です。
- キャストを “点在” させない
- 変換は “境界” で1回に寄せる
- 丸めも “境界” で責任を持つ
例: 内部はdoubleのまま残すが、外へ出す値はdecimalへ寄せる。
static decimal ToMoney(decimal value) => value;
static decimal ToMoney(double value)
{
// 方針はここに寄せる(丸め/桁/許容)
return (decimal)value;
}
重要なのは (decimal) を各所に散らさないことです。
散ると「どこでズレたか」が追えません。
9-4. 金額だけは優先して揃える
混在で一番痛いのは金額です。
- 金額/税/単価/ポイントの型はdecimalへ寄せる(可能な範囲で)
- 丸め規則と丸め位置を決める
- 深部のdouble計算は “境界で吸収” し、点在させない
9-5. 既存を触らない判断も正しい(ただし増殖は止める)
深い部分がdoubleだらけでも、全面的に変えない判断は普通に正しいです。
その代わりに次を守ります。
- 新規追加は「意味に合う型」で書く
- 変換は境界に寄せる(キャスト点在を禁止する)
- 許容誤差/丸め規則は “1箇所” から参照する
「既存はそのまま + 新規も適当に混ぜる」だけは避けます。
10. レビュー用チェックリスト
- 金額/税/単価/利率に float/double が混ざっていないか
- double/floatで
==比較していないか - eps(許容誤差)が散っていないか
- decimalの丸め規則(MidpointRounding)が明示されているか
- 丸め位置(明細/合計/確定点)が揃っているか
- キャストが点在していないか(境界に寄せられているか)
まとめ(このページの使い方)
- 金額は decimal。丸め規則と丸め位置まで決める
- それ以外は double が基本。等価比較を捨てて許容誤差で判定する
- floatは理由がある時だけ。軽さ目的で雑に選ばない
- 既存が混在しているなら、全面改修より 増殖を止めて境界で揃える を優先する