C#のプログラムでは、値が「まだ決まっていない」や「存在しない」を表すnullを安全に扱うことが重要です。
本記事では、null許容型(?とnull合体演算子(??)を中心に、初心者が理解しやすい手順で書き方と注意点を詳しく解説します。
実行可能なサンプルコードと出力も載せていますので、動きの感覚をつかんでください。
C#のnull安全の基本
nullとは何かと初心者向けの考え方
C#におけるnullは、参照先や値が存在しないことを表します。
参照型(stringや配列、クラス型など)はもともとnullを取り得ますが、値型(intやbool、structなど)は通常nullを取りません。
そこで、値型に対しても「値がない」状態を表したいときに使うのが?を付けるnull許容型です。
「空」や「0」は値があるのに対して、nullは「値がない」という意味の違いがあります。
ここを混同しないことがnull安全の第一歩です。
C#でのnull許容型とnull合体演算子の役割
int?やbool?などのnull許容型は、値型にも「ない」という状態を追加します。??(null合体演算子)は、「左がnullなら右を使う」という簡潔な代替値ロジックを実現します。
C# 8以降では#nullable enableを使うと、参照型のnullの可能性もコンパイル時に警告してくれます。
学習段階から有効化しておくと安心です。
#nullable enable
using System;
string? mayBeNull = null; // null許容の参照型(警告なし)
string notNull = "hello"; // 非nullとして扱う参照型
// notNull = null; // 警告: 非nullの参照型にnullを代入しようとしている
null許容型?の書き方と使い方
基本の宣言例(int? string?)
?は型名の直後に付けます。
値型と参照型での例は次のとおりです。
#nullable enable
// 値型のnull許容
int? count = null; // 「まだ数が決まっていない」を表せる
double? ratio = 0.75; // 値が入ることももちろん可能
// 参照型のnull許容(C# 8+ 推奨スタイル)
string? nickname = null; // 「ニックネーム未設定」
string fullName = "Taro Yamada"; // 非nullとして設計
値型に?を付けると、HasValueで「ある/ない」を判定できます。
参照型は== nullでのチェックが一般的です。
初期化とnullチェック(HasValueと== null)
HasValueや== nullで、状態に応じた処理を分けられます。
#nullable enable
using System;
int? points = null;
int? bonus = 10;
if (!points.HasValue)
{
Console.WriteLine("pointsは未設定(null)です");
}
if (bonus.HasValue)
{
Console.WriteLine($"bonusは{bonus.Value}です"); // Valueはnullでないときのみ使用
}
string? memo = null;
if (memo == null)
{
Console.WriteLine("memoはnullです");
}
pointsは未設定(null)です
bonusは10です
memoはnullです
.ValueはHasValue == trueのときだけ使ってください。
nullのときに.Valueへアクセスすると例外が発生します。
メソッドの引数と戻り値でのnull許容
メソッド設計で?を適切に付けると、「nullが来る可能性があるか」を型で示せます。
#nullable enable
using System;
static string FormatUser(string name, int? age)
{
// nameは非null、ageはnullかもしれない
var ageText = age.HasValue ? $"{age.Value}歳" : "年齢不明";
return $"{name} さん ({ageText})";
}
static string? FindNickname(int userId)
{
// 見つからなければnullを返す
return userId == 1 ? "taro" : null;
}
Console.WriteLine(FormatUser("Yamada", 20));
Console.WriteLine(FormatUser("Suzuki", null));
var nick = FindNickname(2);
Console.WriteLine(nick == null ? "ニックネーム未設定" : nick);
Yamada さん (20歳)
Suzuki さん (年齢不明)
ニックネーム未設定
値の取り出しと既定値の指定
null許容値型の取り出しには、次のような方法があります。
HasValueと.Valueを組み合わせるGetValueOrDefault()で既定値を使う??で代替値を指定する(次章で詳解)
#nullable enable
using System;
int? retries = null;
// GetValueOrDefault: nullなら既定値0、引数を渡せば任意の既定値
int r1 = retries.GetValueOrDefault(); // 0
int r2 = retries.GetValueOrDefault(3); // 3
// もちろんHasValueで分岐してValueを使ってもよい
int r3 = retries.HasValue ? retries.Value : -1;
Console.WriteLine($"{r1}, {r2}, {r3}");
0, 3, -1
既定値が明確に決まっている場合はGetValueOrDefaultや??を使うと簡潔です。
null合体演算子??の基本と例
構文と動き(value ?? 代替値)
左辺 ?? 右辺は、左辺がnullなら右辺を採用、そうでなければ左辺をそのまま返します。
左辺の評価は一度だけ行われます。
#nullable enable
using System;
string? userInput = null;
string result = userInput ?? "未入力";
Console.WriteLine(result); // 左がnullなので「未入力」
未入力
文字列と数値でのサンプル
文字列や数値に対して、簡潔に代替値を指定できます。
#nullable enable
using System;
string? nickname = null;
string display = nickname ?? "ゲスト";
Console.WriteLine(display); // ゲスト
int? limit = null;
int max = limit ?? 100; // nullなら100を採用
Console.WriteLine(max); // 100
limit = 50;
Console.WriteLine(limit ?? 100); // 50
ゲスト
100
50
代替値が決まっている場合は??が最短で明瞭です。
if-elseとの違いと置き換えの目安
??はあくまで「nullかどうか」に限定した表現です。
if-elseとの等価関係は次のとおりです。
#nullable enable
using System;
// ?? を if-else で書くとこうなる
string? source = null;
string value1 = source ?? "default";
// 等価な if-else
string value2;
if (source == null)
{
value2 = "default";
}
else
{
value2 = source;
}
Console.WriteLine($"{value1} / {value2}");
default / default
条件が「null判定のみ」なら??を使うと読みやすくなります。
複雑な条件分岐が必要なときはif-elseを使うのがよい判断です。
補足: 代替値を変数へ直接書き戻す??=(null合体代入演算子)もあります。
左辺がnullのときだけ右辺を代入します。
#nullable enable
string? title = null;
title ??= "Untitled"; // titleがnullなら"Untitled"が入る
Console.WriteLine(title);
Untitled
??の連鎖で複数候補から選ぶ
設定値を「環境変数 → 設定ファイル → 既定値」の順で探すといった処理は、??の連鎖で自然に書けます。
#nullable enable
using System;
static string? FromEnv() => null; // 今回は見つからない想定
static string? FromFile() => "fileValue"; // ファイルから取得できた想定
string value = FromEnv() ?? FromFile() ?? "defaultValue";
Console.WriteLine(value); // 最初に非nullだった値が選ばれる
fileValue
最初に見つかった非nullの候補を採用するという意図が、??の連鎖で明快に表現できます。
初心者がつまずきやすい注意点
空文字(“”)とnullは別物
空文字""は「長さ0の文字列」=値はあるのに対し、nullは「値自体がない」です。
表示や保存時の扱いが変わるので注意してください。
次の表は混同しやすい違いを整理したものです。
| 対象 | 値 | 意味 |
|---|---|---|
| 文字列 | "" | 空の文字列。値は存在している |
| 文字列 | null | 文字列が存在しない(未設定) |
#nullable enable
using System;
string? s1 = "";
string? s2 = null;
Console.WriteLine(string.IsNullOrEmpty(s1)); // true
Console.WriteLine(string.IsNullOrEmpty(s2)); // true
// ただし、s1とs2は等価ではない点に注意
Console.WriteLine(s1 == s2); // false
True
True
False
入力検証ではstring.IsNullOrEmptyやstring.IsNullOrWhiteSpaceを使うと、空とnullをまとめて扱いやすくなります。
0とnullは別物(int?に注意)
数値の0は「値はあるがゼロ」、nullは「値が未決定」です。
意味が違うため、検索条件や集計で取り扱いを誤るとバグになります。
#nullable enable
using System;
int? stock = 0; // 在庫は0(品切れ)
int? cost = null; // 原価が登録されていない
Console.WriteLine(stock.HasValue); // true
Console.WriteLine(cost.HasValue); // false
True
False
??の左側は一度評価されることに注意
??は左辺を一度だけ評価します。
副作用のあるメソッドを左辺に置くと、余計な実行が起きないため予測しやすい一方、左辺の評価コストが高い場合はキャッシュ変数に入れてから使うとよいです。
#nullable enable
using System;
int called = 0;
string? Expensive()
{
called++;
Console.WriteLine($"Expensive() 呼び出し回数: {called}");
return null; // 今回は必ずnullを返す想定
}
// 左辺は一度だけ評価される
string value = Expensive() ?? "fallback";
Console.WriteLine(value);
Expensive() 呼び出し回数: 1
fallback
もし同じ左辺式を複数回使うなら、ローカル変数に一度受けると意図がより明確になります。
無闇なnull許容より初期値の設計を優先
型に安易に?を付け過ぎると、どこでもnullチェックが必要になり複雑化します。
次の方針を意識すると安全で読みやすいコードになります。
- 初期値を設計し、非nullが普通の状態になるようにする(例: 空のリスト、空文字、デフォルト構成)
- 本当に「不在」を表したい箇所だけnull許容にする
- 外部入力や検索結果など、存在しない可能性が仕様としてある場所に限定して
?を使う
このバランス設計により、「nullチェックだらけ」や「見落としによる実行時例外」を避けられます。
まとめ
本記事では、null許容型(?とnull合体演算子(??)を中心に、C#のnull安全な扱い方を解説しました。
値型にも「ない」を表す?を付け、HasValueやGetValueOrDefaultで取り出す基本を押さえ、??で代替値を簡潔に指定するのがポイントです。
空文字とnull、0とnullの違いを理解し、??の評価タイミングに注意しながら、安易なnull許容よりも初期値設計を優先してください。
これらを徹底することで、コンパイル時の警告と実行時の例外を最小限に抑え、読みやすく保守しやすいコードへ近づけます。
