【ゲーム開発のためのC#入門講座・基礎強化編】浮動小数点数値型に惑わされないようにしよう【#3】
また難しい漢字の羅列だぜ……
難しい言葉を使わないと死んでしまう病にでもかかっているのか。
それはさておき、浮動小数点数値型とはfloat型とdouble型のことです。
型 | メモリサイズ(byte) | 値の範囲 |
---|---|---|
float | 4 | 正負および桁数により変動 |
double | 8 | 正負および桁数により変動(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桁目 |
---|---|---|---|
1 | 0.5 | 0.25 | 0.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) | 値の範囲 |
---|---|---|
float | 4 | 正負および桁数により変動 |
double | 8 | 正負および桁数により変動(floatより桁数が多い) |
decimal | 16 | 正負および桁数により変動(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) | 末尾に必要な目印 | 例 |
---|---|---|---|
float | 4 | f | 0.2f |
double | 8 | 不要。あるいは明示的にd | 0.2 |
decimal | 16 | m | 0.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#のような言語のデメリットだったりします。
何事も、いいところと悪いところは表裏一体ですね。
それでは、今回もお疲れ様でした!
また次の記事でお会いしましょう!