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?

K28 【鍛錬】合わぬなら 合わせて見せよう そのキャスト float double decimalの選び方

Posted at

連載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.20.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は理由がある時だけ。軽さ目的で雑に選ばない
  • 既存が混在しているなら、全面改修より 増殖を止めて境界で揃える を優先する

関連トピック


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?