【ゲーム開発のためのC#入門講座・基礎強化編】浮動小数点数値型に惑わされないようにしよう【#3】

2022-01-093.0_C#基礎強化編

また難しい漢字の羅列だぜ……

難しい言葉を使わないと死んでしまう病にでもかかっているのか。

それはさておき、浮動小数点数値型とはfloat型とdouble型のことです。

メモリサイズ(byte)値の範囲
float4正負および桁数により変動
double8正負および桁数により変動(floatより桁数が多い)

メモリサイズからもわかりますが、double型はfloat型の桁数多いバージョンです。

この型、何が「浮動」なのかというと小数点の位置が浮動なのです。

メモリサイズが限られているため、扱える桁数には限界があります(floatの場合は7桁ぐらい)。そのうち、整数部分に3桁使うと、小数部分に使えるのは残り4桁です。反対に整数部分が4桁なら、小数部分に使えるのは残り3桁ということになります。

このように整数部分の桁数に応じて小数部分の桁数が変動する(小数点の位置が浮動する)データ種類を浮動小数点数値型といいます

例えば「0.123456」に「10」を足すと、

public class Hello{
    public static void Main(){
        float result = 0.123456f + 10f;
        System.Console.WriteLine(result);
    }
}
出力エリア

10.12346

整数の桁数が増えた分だけ、小数点以下の桁数が四捨五入されて切り捨てられます。

値の範囲が「正負および桁数により変動」となっているのはこれが理由なんですね。

なので浮動小数点数値型を使う時は全体の桁数に注意しておく必要があります。どこかのタイミングで溢れた桁がカットされてしまい、想定通りの結果にならなかった、なんてバグが起こりうるからです。

浮動小数点数値型のもうひとつの注意点

浮動小数点数値型にはもうひとつ、重大な注意点があります。

それは、絶対に表すことのできない数値があるということです。

例えば下記のコードを見てみましょう。

public class Hello{
    public static void Main(){
        System.Console.WriteLine((0.5f + 0.25f) == 0.75f);
        System.Console.WriteLine((0.2f + 0.3f) == 0.5f);
    }
}

普通の感覚でいえば、両方とも実行結果は「true」になりそうですよね。

でも実際にはひとつ目が「true」、ふたつ目が「false」になります。

出力エリア

True
False

うーん、意味がわからない!

ただ、これにもちゃんと理由があります。

それは小数点以下の数値もすべて二進数で管理しているからです。

これは全然覚える必要がないので、そうなんだぐらいの感覚で見て欲しいのですが、二進数における小数点以下の数値を十進数に置き換えると下記のようになります。

整数1桁目小数1桁目小数2桁目小数3桁目
10.50.250.125

基本的に前の桁の半分(1/2)になっていくんですね。

なので二進数の「1.11」は十進数でいうところの「1 + 0.5 + 0.25 = 1.75」、二進数の「1.01」は十進数でいうところの「1 + 0 + 0.25 = 1.25」になります。

さて、それではこの二進数ルールで今回計算しようとした「0.2」や「0.3」を表そうとすると、どうなるでしょう?

0.2の二進数

0.0011 0011 0011 以下エンドレス

0.3の二進数

0.0100 1100 1100 以下エンドレス

無理なんです。各桁の数値を合算してちょうど「0.2」や「0.3」になる組み合わせが存在しないんです。

二進数は整数をすべて表すことができが、小数点以下の数値は表せるものと表せないものがあるんですね。

そのため、この場合は近似値と呼ばれる、二進数で表すことのできる一番近い値が設定されます。

これを踏まえて今回の処理を二進数に置き換えて考えてみると、

(0.5f + 0.25f) == 0.75f
// ↓ 二進数に置き換えても、
//   一致しているのでtrueになる!
(0.1 + 0.01) == 0.11
    
(0.2f + 0.3f) == 0.5f
// ↓ 二進数に置き換えると、
//   近似値の足し算になっていて
//   0.11と一致しないからfalseになる!
(0.00110011…… + 0.01001100……) == 0.11

ということなのです。

想定通りの判定結果にならなかった理由がわかりましたね。

でもそれじゃあ、正確に0.2や0.3で計算したい場合はどうすればよいのでしょう?

そのための型がC#には用意されています。

それがdecimal型です。

decimal型って?

以前ご紹介した代表的な値型の中に、実はfloatとdouble以外にもうひとつ小数点以下の数値を扱える型がありました。

メモリサイズ目安(byte)値の範囲
float4正負および桁数により変動
double8正負および桁数により変動(floatより桁数が多い)
decimal16正負および桁数により変動(10進型)

そう、このdecimal型こそがこの問題解決のために用意された型です。

メモリサイズのところを見るとわかりますが、doubleの2倍、floatの4倍も必要なんですね。その代わり、小数点以下の数値を十進数と同じように管理することができるという特徴があります。

decimal型であれば、先程のプログラムも想定通り動作します。

public class Hello{
    public static void Main(){
        // ↓末尾の「m」はdecimal型で計算してね、という目印
        System.Console.WriteLine((0.2m + 0.3m) == 0.5m);
    }
}
出力エリア

True

金額計算のように正確な数値計算が求められる場合はdecimal型、多少ざっくりの計算でも問題ない場合はfloatまたはdouble型、といった感じで用途に応じて使い分けていくとよいでしょう。

小数点以下の数値を扱える型まとめ

メモリサイズ(byte)末尾に必要な目印
float4f0.2f
double8不要。あるいは明示的にd0.2
decimal16m0.2m

記載にある通り、小数点以下の値がある数値の末尾に目印をつけなかった場合はdouble型として扱われる点に注意。

Unityでの活躍ポイント

以前にもお話しましたが、Unityでの座標や拡大率などは大抵float型になっています。

そのため、座標計算や座標判定を行う際に、今回学んだことを知っていないと、想定外のバグに悩まされてしまうことがあります。

また、スコア表示などの処理においても、計算結果を表示しようとしたら何故か「1.200002」という謎の数値が表示されてしまった、なんてこともあります。

これも今回学んだことを思い出せれば「近似値だからうまくいかないのか」とすぐに原因に気付くことができます。

このように、C#というプログラミング言語の特性を正しく理解しておくことは、ゲーム開発におけるバグの原因調査や対応速度に大きく貢献してくれるんですね。

実践演習

それでは実際に今回学んだを使い分けてみましょう。

演習①

仕様

下記のプログラムは末尾に目印がないことから、double型のデータをfloat型やdecimal型の変数に格納しようとしていると判断されてしまい、エラーになってしまいます。
正しくプログラムが動くよう、数値の末尾に適切な目印をつけてあげてください。

テンプレート
public class Hello{
    public static void Main(){
        float result1 = 1.5 + 1.5;
        decimal result2 = 0.5 + 0.5;
        System.Console.WriteLine(result1);
        System.Console.WriteLine(result2);
    }
}

演習②

仕様

下記のプログラムは、末尾の目印はプログラマーの想定通りの設定になっています。ただし今度は格納する変数の型を間違えてしまいました。
正しくプログラムが動くよう、変数「result1」と「result2」の型を適切に変更してあげてください。

テンプレート
public class Hello{
    public static void Main(){
        int result1 = 1.5f + 1.5f;
        decimal result2 = 0.5d + 0.5d;
        System.Console.WriteLine(result1);
        System.Console.WriteLine(result2);
    }
}

答え合わせ

演習①の答え

public class Hello{
    public static void Main(){
        float result1 = 1.5f + 1.5f;
        decimal result2 = 0.5m + 0.5m;
        System.Console.WriteLine(result1);
        System.Console.WriteLine(result2);
    }
}

演習②の答え

public class Hello{
    public static void Main(){
        float result1 = 1.5f + 1.5f;
        double result2 = 0.5 + 0.5;
        System.Console.WriteLine(result1);
        System.Console.WriteLine(result2);
    }
}

まとめ

  • 浮動小数点数値型とはfloat型とdouble型のこと
  • 有効桁数が限られているため、整数部分の桁数に応じて小数部分の桁数が変動する(小数点の位置が浮動する)という特性がある
  • 小数点以下の数値も二進数で管理しているため、絶対に表すことのできない数値がある。それらは近似値で扱われる
  • 正確に計算したい時は、小数点以下の数値も十進数と同じように管理してくれるdecimal型を使おう

実践演習はいかがだったでしょうか?

色んな型があることでメモリに優しい反面、ちゃんと型を合わせてあげないとエラーになってしまうのが結構面倒ですよね。これが明示的に型を宣言するC#のような言語のデメリットだったりします。

何事も、いいところと悪いところは表裏一体ですね。

それでは、今回もお疲れ様でした!

また次の記事でお会いしましょう!

Posted by yuumekou