C#の変数・データ型・定数:.NET 10で確かめた範囲と落とし穴

C#の変数宣言、整数・浮動小数点型の実際の範囲、var、null許容型、default、constとreadonlyの違いを.NET 10で再検証。0.1+0.2の実際の結果、decimalとdoubleの速度差、constの落とし穴も実測で確認。

2026年11月改訂: 2021年に韓国語で書いた変数・データ型の講座を、日本語向けに書き直しました。例はすべて .NET 10.0.14(C# 14)でコンパイルし直しています。どれも動きますが、元の表の long の範囲は桁が1つずつ足りず、float・double の出力は2021年より桁が増え、float.Parse("123.456") はドイツ語ロケールのマシンで 123456 を返します。さらに、2021年の入門講座になかった var、0.1 + 0.2、null 許容値型、default、const と readonly の違い、decimal と double の速度の実測を加えました。

C#を学び始めた人や、PythonやJavaScriptから移ってきて「数値型が多すぎて、どれを選べばいいのか」と迷っている人向けの記事です。先に結論を書くと、整数は int、測定値は double が基本で、約21億を超えうるなら long、金額のように10進の小数を正確に扱うなら decimal を使います。ただし decimal は今回の実測で演算によって6〜45倍遅くなりました。

変数の宣言と、2021年から変わったこと

変数は、型が決まった名前付きの格納場所です。型と名前(識別子)で宣言し、宣言と同時に初期化もできます。

int a;          // 宣言だけ
a = 1000;       // 代入
int b = 2000, c = 3000;   // 同じ型ならまとめて宣言できる
Console.WriteLine(a);
1000

2021年版はすべての例を using 4行と namespace ConsoleApplication1 { class Program { static void Main… } } で包んでいました。C# 10(.NET 6)以降、新しいコンソールプロジェクトはトップレベルステートメントと暗黙の using を使うので、上の5行が Program.cs のすべてです。昔の書き方もそのままコンパイルできます。出力の最後にあった「続行するには何かキーを押してください」に当たる行は Visual Studio が付けたもので、dotnet run では出ません。

元の説明を2点訂正します。

  • int を「10進数の整数型」と説明していましたが、int は 32ビットの2の補数による2進整数です。10進数はリテラルの書き方にすぎず、0x3E8 も 0b1111101000 も 1000 と同じ値です。
  • int a がアドレス 00120B14〜00120B17 の4バイトを占める図がありましたが、削除しました。アドレスの列で 00120B12 が抜けていたうえ、.NET のローカル変数はふつう CPU レジスタかスタックフレームに置かれ、決まったアドレスを当てにできないからです。

C と違い、代入していないローカル変数は読めません。int x; Console.WriteLine(x); はゴミ値ではなく error CS0165: Use of unassigned local variable 'x' というコンパイルエラーです。

var:型を省略しても型は固定

var は初期値からコンパイラが型を決めます。動的型ではなく、静的型のままです。

var v1 = 10; var v2 = 10L; var v3 = 10.0; var v4 = 10f; var v5 = 10m;
var v6 = 3_000_000_000;
Console.WriteLine($"{v1.GetType().Name} {v2.GetType().Name} {v3.GetType().Name} {v4.GetType().Name} {v5.GetType().Name} {v6.GetType().Name}");
Int32 Int64 Double Single Decimal UInt32

最後がひっかけです。3_000_000_000 は int に収まらないので、long ではなく uint のリテラルになります。int i = 3_000_000_000; と書くと error CS0266: Cannot implicitly convert type 'uint' to 'int' です。数字の区切りの _ は C# 7.0 から使えます。

var x; は error CS0818(暗黙的に型指定された変数は初期化が必要)、var y = null; は error CS0815(null は暗黙的に型指定された変数に代入できない)になります。

数値型と実際の範囲

表を書き写す代わりに、.NET 10 で MinValue/MaxValue を出力しました。

型 サイズ(バイト) 範囲(.NET 10 の出力)
byte / sbyte 1 0〜255 / -128〜127
short / ushort 2 -32,768〜32,767 / 0〜65,535
int / uint 4 -2,147,483,648〜2,147,483,647 / 0〜4,294,967,295
long / ulong 8 -9,223,372,036,854,775,808〜9,223,372,036,854,775,807 / 0〜18,446,744,073,709,551,615
float 4 ±3.4028235E+38、有効桁数は約7桁
double 8 ±1.7976931348623157E+308、約15〜17桁
decimal 16 ±79,228,162,514,264,337,593,543,950,335、28〜29桁
char 2 UTF-16 のコード単位1つ
bool 1 true / false

2021年の表の long の範囲「-922337203685477508〜922337203685477507」は、両端とも1桁足りませんでした。正しくは約±922京(9.22×10^18)です。decimal の「±1.0x10e-28〜±7.9x10e28」も、10e28 は10^29の意味になるので、±1.0×10^-28〜±7.9×10^28 と書くのが正確です。

2021年以降に増えた型は、nint/nuint(C# 9、ネイティブサイズの整数。64ビットプロセスでは8バイト)と、ライブラリ型の Int128・UInt128(.NET 7)、Half(.NET 5)です。Int128.MaxValue は 170141183460469231731687303715884105727 と表示されました。

型名の先頭の u は unsigned、つまり符号ビットがないという意味です。負の数を表していたビットパターンを大きな正の数に回すので、正の範囲が倍になります。

判断図。出発点は「保存したい数値」。問1:常に整数か。はいなら問2:±約21億に収まるか。はいなら int、いいえなら long。整数でないなら問3:金額のように10進の小数を正確に扱う必要があるか。はいなら decimal、いいえなら double。
数値型の選び方。基本は int と double で、範囲か正確さの要件があるときだけ別の型に替える。

float・double・decimal と 0.1 + 0.2

2021年の浮動小数点の例はそのままコンパイルできますが、出力が変わりました。

float a = 123.567891011f;
double b = 1234.567878910111213;
decimal c = 1234.56789101112131415161718m;
Console.WriteLine("a={0}, b={1}, c={2}", a, b, c);
a=123.567894, b=1234.5678789101112, c=1234.56789101112131415161718

2021年版では a=123.5679, b=1234.56787891011 でした。.NET Core 3.0 から float/double の ToString() が、元の値に戻せる最短の文字列を出すように変わったためです。精度が変わったのではなく表示が変わっただけですが、.NET Framework 時代のコードで書式化した文字列を比べていると、移植時にここで壊れます。

接尾辞 f、m を外すと .NET 10 は次のように報告します。元記事のエラーにはコード番号がなく、個人フォルダーのパスも写っていたので、その部分は省きました。

error CS0664: Literal of type double cannot be implicitly converted to type 'float'; use an 'F' suffix to create a literal of this type
error CS0664: Literal of type double cannot be implicitly converted to type 'decimal'; use an 'M' suffix to create a literal of this type

元のコードは 123.567891011 f のように接尾辞の前に空白が入った状態で載っていました。そのままコピーすると error CS1003: Syntax error, ',' expected です。接尾辞は数字に続けて書きます。

では、有名な計算です。

double d = 0.1 + 0.2;
Console.WriteLine($"{d}  {d == 0.3}");
Console.WriteLine($"{0.1m + 0.2m}  {0.1m + 0.2m == 0.3m}");
double sum = 0; for (int i = 0; i < 10; i++) sum += 0.1;
Console.WriteLine(sum);
Console.WriteLine($"{Math.Abs(d - 0.3) < 1e-9}");
0.30000000000000004  False
0.3  True
0.9999999999999999
True

double は2進数の小数を持ちます。10進数で1/3が割り切れないのと同じく、0.1は2進数で割り切れません。decimal は10進の仮数と指数を持つので0.1が正確です。とはいえ万能ではなく、1m / 3 * 3 は 0.9999999999999999999999999999 でした。float は整数の精度も早く尽きて、16777216f + 1 は 16777216 のままでした。

実測:decimal はどれくらい遅いか

正確さにはコストがあります。小数第2位までの価格配列から1,000万個を取り出して累計するループを型ごとに作り、Release ビルド、Stopwatch、[MethodImpl(NoInlining)]、ウォームアップ1回のあと3回計測しました(Apple Silicon Mac、M1 Pro)。乗算の 1.08 は軽減税率8%の税込計算を想定した値です。

演算(1,000万回) double decimal 倍率
s += p[i] 9.7〜9.9ms 56.2〜57.2ms 約6倍
s += p[i] * 1.08 9.8〜10.2ms 69.9〜72.6ms 約7倍
s += p[i] / 3 9.8〜10.1ms 447.2〜457.8ms 約45倍
棒グラフ。加算:double 9.8ms、decimal 56.8ms。乗算:double 10.0ms、decimal 71.2ms。除算:double 9.9ms、decimal 452.6ms。decimal は加算・乗算で約6〜7倍、除算で約45倍遅い。
1,000万回の加算・乗算(×1.08)・除算(÷3)を累計した時間。3回の平均。.NET 10.0.14、Release ビルド、Apple Silicon Mac(M1 Pro)。短いほど速い。

同じ実行で、そのコストを払う理由も見えました。価格1,000万個の正確な合計は 4924253250.72 ですが、double の合計は 4924253250.706993 で、すでに小数第2位がずれていました。請求額や残高は decimal、センサー値・座標・統計は double が基本です。

型変換と文字列のパース

double を int にキャストすると0方向に切り捨てられます。(int)123.45 は 123、(int)-123.45 は -123 です。四捨五入したいときは Math.Round ですが、既定は銀行型丸めなので Math.Round(2.5) は 2、Math.Round(3.5) は 4 でした。範囲を超えるキャストは C#の演算子 の記事で扱います。

ToString() と Parse は2021年と同じく動きますが、どちらも現在のカルチャに従います。日本語環境(ja-JP)の小数点はピリオドなので気づきにくいのですが、小数点にカンマを使うドイツ語設定のサーバーでは次のようになります。

var de = CultureInfo.GetCultureInfo("de-DE");
Console.WriteLine(float.Parse("123.456", de));
Console.WriteLine(float.Parse("123.456", CultureInfo.InvariantCulture));
123456
123.456

例外も出ずに1,000倍の値が入ります。データファイル、API のレスポンス、設定値には必ず CultureInfo.InvariantCulture を渡し、ユーザー入力には TryParse を使います。int.Parse("12a") は FormatException: The input string '12a' was not in a correct format. を投げますが、int.TryParse("12a", out var n) は false を返すだけです。

object に何でも入るのはすべての型が System.Object を継承しているから、という元の説明は正しいです。補足すると、値型を object に入れるとボックス化(ヒープ割り当て)が起き、object a = 12345678910; は System.Int64 として格納されました。ボックス化のコストは C#のジェネリック の記事で測っています。

null 許容値型と default

int などの値型には null を入れられません。? を付けると Nullable<int> になります。

int? n = null;
Console.WriteLine($"{n.HasValue} {n ?? -1} {n.GetValueOrDefault()}");
Console.WriteLine($"{(n + 1) is null} {n < 5} {n >= 5}");
int x = n.Value;   // コンパイル時に warning CS8629、実行時に例外
False -1 0
True False False
Unhandled exception. System.InvalidOperationException: Nullable object must have a value.

null との演算結果は null、null との比較はすべて false です。n < 5 と n >= 5 が両方 false になる点に注意してください。.Value の行でコンパイラは warning CS8629: Nullable value type may be null. を出しました。

C# 7.1 の default リテラルは、どの型でもゼロに当たる値を返します。int は0、bool は false、DateTime は 0001-01-01、参照型は null です。

const と readonly

2021年に説明した const のルールは変わっていません。エラーコードは次のとおりです。

k = 60;           // error CS0131: The left-hand side of an assignment must be a variable, property or indexer
const int a;      // error CS0145: A const field requires a value to be provided

const に書けるのはコンパイル時定数(組み込み型か文字列)だけで、public const DateTime T = DateTime.Now; は error CS0283: The type 'DateTime' cannot be declared const です。実行時に決まる値は static readonly を使い、このフィールドには宣言部か静的コンストラクターでしか代入できません(それ以外は error CS0198)。

もっと大事な違いはバージョン管理です。コンパイラは const の値を、それを使うアセンブリの中にコピーします。ライブラリに public const int MaxItems = 10; と public static readonly int MaxItemsRo = 10; を置き、両方を表示するアプリをビルドしてから、両方を50に直してライブラリの DLL だけを差し替えました。

差し替え前: const=10 readonly=10
差し替え後: const=10 readonly=50

アプリをビルドし直すまで、const は古い値のままでした。円周率や12か月のように絶対に変わらない値、または1つのプロジェクト内だけで使う値は const、ライブラリや NuGet パッケージで公開する値は static readonly が安全です。

まとめ:場面別の選び方

用途 選ぶもの
カウンター、インデックス、たいていの整数 int
21億を超える ID、タイムスタンプ、ファイルサイズ long
金額、正確な10進小数 decimal(実測で6〜45倍遅い)
測定値、科学計算、グラフィックス double、比較は許容誤差で
値型で「まだ値がない」を表す int?、double? など
1プロジェクト内の固定値 const
公開する値、変わりうる値、実行時に計算する値 static readonly
データファイルのパース Parse(s, CultureInfo.InvariantCulture) か TryParse

続きは、演算子とオーバーフローの C#の演算子、分岐の if と switch、複数の値を扱う C#の配列 へどうぞ。

検証環境:.NET SDK 10.0.403 / ランタイム 10.0.14、C# 14(net10.0 の既定)、macOS、Apple Silicon Mac(M1 Pro)。

よくある質問

C#で0.1 + 0.2が0.3にならないのはなぜ?

double は2進数の小数で値を持ち、0.1も0.2も2進数では割り切れません。.NET 10 では 0.1 + 0.2 は 0.30000000000000004 と表示され、(0.1 + 0.2 == 0.3) は False でした。decimal なら 0.1m + 0.2m はちょうど 0.3 です。double 同士は Math.Abs(a - b) < 1e-9 のように許容誤差で比べます。

const と static readonly はどちらを使うべき?

const の値はコンパイル時に、それを使うすべてのアセンブリへコピーされます。ライブラリの const を10から50に変えて DLL だけ差し替えたところ、アプリは10のままで、static readonly は50になりました。決して変わらない値や1プロジェクト内だけの値は const、公開 API の値や DateTime のように実行時に決まる値は static readonly にします。

decimal は double よりどれくらい遅い?

.NET 10 の Release ビルド(Apple Silicon Mac)で1,000万回の加算は double 9.8ms、decimal 56.8ms、乗算は10.0ms対71.2ms、除算は9.9ms対452.6msでした。加算・乗算で約6〜7倍、除算で約45倍です。金額計算なら正確さのために払う価値があり、物理・グラフィックス・統計なら double が向いています。

  • #C#
  • #変数
  • #データ型
  • #decimal
  • #const
  • #.NET

t.laboratory

実際に作って運用しているアプリ・サーバー・サービスで起きた問題と、その解決に使ったコードを記録する技術ブログです。

運営者情報 · アプリ

↑