本文へスキップ
ウェブエンジニア問題集
第10章

C#のnull安全 — null許容参照型と?.・??・!の使い方

約8分
この章の目次開く

NullReferenceException は、C#で最も多く遭遇する実行時エラーです。「値が入っていると思って使ったら null だった」という、どの言語でも起きる問題です。

現在のC#には、これをコンパイル時の警告として検出する仕組みがあります。TypeScriptの strictNullChecks に近い考え方です。

null許容参照型

.csproj に <Nullable>enable</Nullable> が書かれていると、参照型は既定で「nullを入れてはいけないもの」として扱われます。

string name = null;    // 警告:null を代入している
string? nickname = null;  // OK:null を許容する型
csharp
書き方意味
stringnullは想定しない
string?nullがありうる
型に ? が付いているかどうかが、「nullを考慮すべきか」の設計上の宣言になります。

string? の値をそのまま使おうとすると、警告が出ます。

string? nickname = GetNickname();
// Console.WriteLine(nickname.Length);  ← 警告:null の可能性
 
if (nickname is not null)
{
    Console.WriteLine(nickname.Length);  // OK
}
csharp

値型のnull許容(Nullable値型)

int や DateTime などの値型は、そもそも null を持てません。nullを表したい場合は ? を付けます。

int? count = null;
 
if (count.HasValue)
{
    Console.WriteLine(count.Value);
}
csharp
メンバー意味
HasValue値が入っているか(bool)
Value中身。nullのときにアクセスすると例外
GetValueOrDefault()nullなら既定値を返す

int? の正体は Nullable<int> という構造体で、参照型の string? とは仕組みが異なります。ただし書き方はほぼ共通なので、使う側は同じ感覚で扱えます。

nullを扱う演算子

演算子名前動作
?.null条件演算子左がnullなら、その先を評価せずnullを返す
??null合体演算子左がnullなら右の値を使う
??=null合体代入左がnullのときだけ代入する
!null免除演算子「nullではない」とコンパイラに伝える
string? name = user?.Profile?.Name;      // 途中がnullでも例外にならない
string display = name ?? "名無し";        // nullなら既定値
name ??= "未設定";                        // nullのときだけ代入
csharp

?. は途中でnullが出た時点で全体がnullになるため、user.Profile.Name のような連鎖アクセスを安全に書けます。JavaScriptのオプショナルチェーンと同じ挙動です。

想定外に驚くイメージ
NullReferenceExceptionは、想定と実際のデータがずれたときに現れます

! は最後の手段

string name = GetName()!;  // 「絶対にnullじゃない」と宣言する
csharp

! を付けると警告は消えますが、実際にnullだった場合は実行時に例外になります。コンパイラのチェックを黙らせているだけで、安全になったわけではありません。

学習者学習者

警告が消えるならつい使いたくなります……使っていい場面はあるんですか?

「直前で確認済みだが、コンパイラがそれを追跡できない」ケースに限ります。たとえばテストコードや、外部ライブラリの型注釈が不正確な場合です。通常のコードでは、is not null で分岐するか、後述の例外を投げる形にしてください。

引数のnullチェック

publicなメソッドでは、渡された値がnullでないことを明示的に確認します。

構文: ArgumentNullException.ThrowIfNull(argument)

引数渡せるもの説明
argument(第1引数)任意の参照型nullなら例外を投げる対象。引数名は自動で埋められる

戻り値: なし(nullの場合は ArgumentNullException)

public void Register(User user)
{
    ArgumentNullException.ThrowIfNull(user);
    // 以降、user は null ではないものとして扱える
}
csharp

「nullのまま処理を進めて、遠く離れた場所で例外になる」より、入口で落としたほうが原因を特定しやすくなります。例外の扱いは C#の例外処理 で扱います。

プロパティを必須にする

初期化し忘れによるnullを防ぐには、required を使います(C# 11以降)。

public class User
{
    public required string Name { get; init; }
    public string? Nickname { get; init; }
}
 
var user = new User { Name = "田中" };  // Name を省略するとコンパイルエラー
csharp
「必ずある値」は required、「無いこともある値」は ? を付ける。この2つでクラスの前提を型に書き込めます。

文字列のnullと空文字

文字列は「null」「空文字」「空白のみ」の3状態があり、判定用のメソッドが用意されています。

メソッドnull""" "
string.IsNullOrEmpty(s)truetruefalse
string.IsNullOrWhiteSpace(s)truetruetrue

ユーザー入力の検証では、空白だけの入力も弾きたいことが多いため IsNullOrWhiteSpace を使います。

よくあるハマりどころ

外部から来たデータを信用する

JSONのデシリアライズやデータベースからの読み込みでは、型が string でも実際にはnullが入りうるためです。null許容参照型のチェックはC#のコードだけを見ており、外部データの中身までは保証しません。境界では必ず自分で検証する必要があります。

?. を付けたのに例外になる

var length = user?.Name.Length;  // Name が null なら例外
csharp

?. は直前の user にしか効きません。連鎖するすべての箇所に付ける必要があります(user?.Name?.Length)。

値型に ?. を付けて型が変わる

int? に対して演算すると、結果も int? になります。int として使いたい場合は ?? 0 や GetValueOrDefault() で確定させます。

警告を一括で無効化する

プロジェクト全体で <Nullable>disable</Nullable> に戻すと、この仕組みの利点は失われます。既存プロジェクトへ段階的に導入する場合は、ファイル単位の #nullable enable から始めるのが現実的です。

先生先生

新規プロジェクトなら最初から有効のまま進めるのがいちばん楽。あとから入れるほうが何倍も大変なんだ。

ちゃんと使うためのポイント

  • string は「nullでない」、string? は「nullがありうる」という宣言
  • 値型でnullを表すなら int?(Nullable<int>)
  • ?. ?? ??= を組み合わせて安全にアクセスする
  • ! は警告を消すだけで安全にはならない。使用は最小限に
  • publicメソッドの入口は ArgumentNullException.ThrowIfNull で守る
  • 必須の値は required、任意の値は ? を付けて型に意図を書く
  • 外部から来たデータは、型注釈を信用せず自分で検証する

次の章では、入口のチェックで投げた例外そのものを扱います。try / catch / finally と using の関係を整理します。

参考リンク