C#入門編

C#入門編(19)null許容型(Nullable)入門 ~null参照例外を防ぐ~【Visual Studio 警告対策・null演算子】

今回は「null」について解説します。

「null」はプログラミングでよく使われる概念ですが、バグの原因になることも多く、nullの発明者であるアントニー・ホーアは「10億ドルの過ち」と呼びました。

本記事では以下について説明します。

  • nullが引き起こす問題とその危険性
  • C#におけるnullとの戦いの歴史(null関連の演算子)
  • モダンC#でnull安全にコードを書く方法(null許容型とは?)

以下のような方に役立つ内容となっています。

  • null許容型とビルド時のnull関連の警告をどう扱えばよいかわからない
  • nullの理解を深め、安全なC#コードを書きたい
  • C#におけるnull関係の記法をまとめて学びたい
プロ太

モダンなC#開発では、null関連の誤りはビルド時に厳しくチェックされて多くの警告がでるため、初心者が戸惑いやすい点かと思います。

今日は、C#におけるnullとの戦いの歴史をみながら、一緒にnullへの理解を深め、うまく扱えるようになりましょう!

動画は以下になります。

なぜnullは怖いのか?

参照型が null のままメンバーにアクセスすると null参照例外が発生します。

string? msg = null;
Console.WriteLine(msg.Length);

Visual Studioでデバッグ実行すると以下のようになりますね。

複雑なプログラムでは、null値が渡されてくることを想定しておらず、nullチェックが漏れていてこのようなエラーが発生することがあります。

C#に限らずPython・JavaScript/TypeScript、Javaなどでもnull参照例外と同種のエラーが存在し、プログラマならば必ず遭遇して悩まされているでしょう。

プロ美

使っているアプリでnull参照例外エラーに遭遇することもあるよね…。

プロ太

アントニー・ホーアがnullを「“10 億ドルの過ち”」と呼んだのは、まさにこのような状況があるためですね。

C#におけるnullとの戦いの歴史をざっくり俯瞰

C#におけるnullとの戦いの歴史を概観すると以下のようになります。

フェーズ時期主な出来事状況
Phase ①
nullチェック期
2002‑2004
(C# 1)
・参照型は常にnull 可能・null例外多発
・冗長なnullチェック
Phase ②
シンタックス救済期
2005〜2014
(C# 2-5)

・null合体演算子(??)の導入
・値型で
Nullableが登場
・nullチェックが若干簡潔に
・まだ“nullチェック書き忘れ”は防げない
Phase 
シンタックス高度化期
2015〜2018
(C# 6-7)
・null条件演算子(?.)の導入・nullチェックコードがより簡潔に
Phase 
ビルド時チェック期
2019
(C# 8)
・null許容参照型とビルド時チェック
(革命的な機能)

・null合体演算子の複合代入(??=)の導入
・”チェック書き忘れ”をビルド時に警告で気付ける
Phase 
初期化安全期
2020‑2023(C# 9‑11)init setter/
record/requiredの導入
・オブジェクト生成直後から非nullを保証
プロ太

この後、それぞれをもう少し詳しくみていきましょう。

特に「Phase ④」は歴史の転換点であり重要です。

プロ美

ビルド時のnull関連の警告がたくさん出るのって、この革命(Phase④)の影響なんだね。

Phase① nullチェック期:2002-2004 (C# 1)

null参照例外を避ける方法は、基本的には、プログラマが気をつけてnullチェックを毎回行うという状況でした。

if (obj != null){
    ...
}
プロ太

null参照例外が発生しないよう、開発者が頑張ってnullチェックのコードを書いていた…という感じですね。

if(nullチェック){…}というコードをあちこちに記述する必要があり、可読性もあまり良くない状況でした。

Phase② シンタックス救済期:2005〜2014 (C# 2-5)

null合体演算子

少しでもnullチェック記述の負担を減らすため、null合体演算子(??)が導入されました。

例えば、従来以下のようにnullチェックしていた記述について、

string name = GetName();
if (name == null)
{
    name = "デフォルト名";
}

null合体演算子を使うと以下のように簡潔に記述可能になります。「X ?? Y」は、XがnullであればYを返し、nullでなければそのままXを返します。

string name = GetName() ?? "デフォルト名";
プロ美

なるほど。よくあるnullチェックの処理を少しだけ簡単に書けるようになったんだね。

値型のnull許容

もともとnullは参照型の変数にのみ許容されていましたが、Nullable<T>の導入により、値型についてもnullを許容可能となりました。

以下は、値型をnull許容にする例です。Nullable<int>とすると、整数値の他にnull値も表現可能となります。(int?という簡易記法もあわせて導入されました。)

// C# 2.0以降で使用可能
Nullable<int> value1 = null;
int? value2 = null;  // 上記と同じ意味

データベースからの読み込みやAPIレスポンス処理では「値が存在しない」こともあるため、値型(int、bool、DateTime等)でもnull値を取れるようにしたわけですね。

プロ太

nullを許容する外部データソースとの整合性のために必要なことでした。

一方で結果的にC#におけるnullの使用範囲が広がり、より高度なnull安全対策の必要性が高まったともいえます。

Phase ③ シンタックス高度化期
:2015〜2018(C# 6-7)

null条件演算子の導入

この時期には、null条件演算子(?.)が導入され、プロパティやメソッドのチェーンアクセス時のnullチェックの記述がさらに簡潔になりました。

例えば、従来以下のようにnullチェックをしていましたが、

Person person = GetPerson();
string name = null;
if (person != null)
{
    name = person.Name;
}

null条件演算子で以下のように記述できます。「X?.Y」は、Xがnullであればnullを返し、nullでなければYへアクセスし、その結果を返します。

Person person = GetPerson();
string name = person?.Name; 

以下のように、インデクスアクセス、メソッド呼び出しなどでも使えます。

//インデクスアクセス
string firstItem = names?[0]; // namesがnullならばnullを返す

//メソッド呼び出し
string result = obj?.GetValue();  // objがnullならnullを返す

//null条件演算子とnull合体演算子の組み合わせ
int length = text?.Length ?? 0; // textがnullなら0、そうでなければtext.Length

//条件演算子が複数ある場合(チェーンアクセス)
string city = person?.Address?.City;  // personかAddressがnullを返す

//イベントの安全な発火
PropertyChanged?.Invoke(this, args);  // PropertyChangedがnullでなければ発火

このフェーズでは、nullチェックの記述が大幅に簡略化され、コードの可読性が向上しました。

プロ太

しかし、まだ開発者がnullチェックを書くことを忘れるという根本的な問題は解決されていませんね。

プロ美

簡潔に書けるようになったのはいいけど、チェックすること自体を忘れたら意味がないよね。次のフェーズではそこがどう改善されたのかな?

Phase ④ ビルド時チェック期:2019 (C# 8)

C# 8.0では、nullの扱いに関して革命的な機能が導入されました。

それが「null許容参照型(Nullable Reference Types)」です。これにより、ビルド時コンパイル時)にnullの危険性を検出できるようになりました。

null許容参照型の導入

従来、C#では参照型(string, classなど)は常にnull値を持てました。

しかし、C# 8.0からは参照型を「null非許容(non-nullable)」と「null許容(nullable)」に区別可能になりました。null許容の場合は「型名?」と書きます。

string nonNullable = "値"; // null非許容(nullを代入するとコンパイル警告)
string? nullable = null;   // null許容(明示的にnullを許容)

この機能を有効にするには、プロジェクトファイル(.csproj)へ設定を追加するか、ファイル先頭で #nullable enable ディレクティブを記述します。

プロ太

C#9.0以降のプロジェクトテンプレートでは、null許容参照型がデフォルトで有効化されており、使うことが推奨されています。

null許容参照型を導入すると、ビルド時にコンパイラが警告を表示してくれます。

string name = null; // (1)警告: null非許容の変数にnullを代入しています

string? nullableName = GetName(); //GetName()の戻り値はstring?
int length = nullableName.Length; // (2)警告: nullの可能性がある変数の直接参照

それぞれ以下の警告がでます。

プロ美

nullチェックの書き忘れをアプリ実行前のビルド時に教えてくれるってことだね。確かに革命的かも!

(1)の警告については、nullを代入したい場合は「string? name」とnull許容型にすれば解消されます。もしくはnull非許容にしたいのであれば、nullを代入しないようにします。

(2)の警告(nullの可能性がある変数の参照)については以下の3つの解消方法があります。

1.nullチェックを追加

string? nullableName = GetName();
if (nullableName != null)
{
    int length = nullableName.Length; // 警告が出なくなる
}

nullチェック自体はこのようにどこかで必要となります。

2.null許容型を使用する

string? nullableName = GetName();
int? length = nullableName?.Length; // null許容チェーンで安全にアクセス

ある意味、nullチェックの先延ばしですね。lengthを参照する際にはチェックが必要です。

3.null免除演算子(!)

C#8.0では「絶対にnullではない」と開発者が確信できる場合に使用するnull免除演算子(!)も導入されました。

null許容型となる変数「X」について「X!」とすると、X!はnull非許容となります。

string nonNullableName = GetName()!;  // GetName()の戻り値はnull許容型string?
int length = nonNullableName.Length; 

これは「GetName()の戻り値はnull許容型になってるけど、私はnullではないと信じています」という意思表示です。

もし実際にnullだった場合、実行時にnull参照例外が発生します。

プロ太

この方法は、ある意味、せっかくのビルド時チェック機能を迂回する方法なので、乱用はしないほうがよいでしょう。

null合体代入演算子(??=)

C# 8.0では、null合体演算子の拡張としてnull合体代入演算子(??=)も導入されました。

例えばC#8.0より前では、null合体演算子で以下のように記述していましたが、

string? name = null;
…
name = name ?? "デフォルト名";

以下のように記述可能になりました。

string? name = null;
…
name ??= "デフォルト名";
プロ美

「a = a + b」を「a+=b」って書けるのと同じような感じだね。

Phase ⑤ 初期化安全期:2020‑2023(C# 9‑11)

C# 9.0以降では、null安全性をさらに向上させるため、特にオブジェクト初期化子に関する新機能が導入されました。

これらは、「オブジェクトを作成した瞬間から非nullを保証する」という考え方に基づいています。

オブジェクト初期化子について

オブジェクト初期化子とは、オブジェクトの作成と同時にプロパティに値を設定できる機能で、C# 3.0で導入されています。

これにより、コードがより簡潔になります。

// オブジェクト初期化子の例
var person = new Person { Name = "田中"};

// 上記は以下と同等
var person = new Person();
person.Name = "田中";

ただし、C#8.0まではnull非許容型とオブジェクト初期化子は相性が悪く、以下のような場合には「コンストラクタで初期化されていない」という警告が出てしまいます。

public class Person
{
    public string Name { get; set; } // null非許容型のNameをコンストラクタで初期化しないと警告
}

var person = new Person { Name = "田中" };
プロ太

この場合、「new Person();」でNameが初期化されていないインスタンスを生成できてしまう危険があるため、警告が出ること自体は妥当ですね。

オブジェクト初期化子を使わず初期化しようとしたときにだけ間違いを検出してくれれば…という感じですね。

required修飾子

C# 11.0では、「required」修飾子が導入されました。これはnull非許容参照型とオブジェクト初期化子を組み合わせた場合の課題を解決するものです。

// 必ず初期化が必要と明示
public class Person
{
    public required string Name { get; set; }
}

// 正しい使い方
var person = new Person { Name = "田中" }; // OK

// エラー - Nameプロパティが初期化されていない
var person = new Person(); // コンパイルエラー

これにより、オブジェクト初期化子を使った場合でも「初期化忘れ」をコンパイル時にエラーとして検出できるようになりました。

プロ美

「required」があると、「このプロパティは必ず初期化してください」とコンパイラに伝えられるんだね!

関連する初期化機能の強化

C# 9.0以降では、null安全性の向上だけでなく、オブジェクトの初期化に関する機能も強化されました。代表的なものとして以下の機能があります。

init-only プロパティ(C# 9.0)

オブジェクト初期化時のみ値を設定でき、その後は変更できないプロパティを定義できます。

public class Person
{
    // オブジェクト初期化時のみ設定可能
    public string Name { get; init; }
}

var person = new Person { Name = "田中" }; // OK
person.Name = "鈴木"; // コンパイルエラー - 初期化後は変更不可

レコード型(C# 9.0)

データを表現するための型で、少ないコード量でイミュータブル(不変)なデータ構造を作成できます。

定義したプロパティは自動的にinit-onlyとなり、初期化時のみ値を設定でき、それ以降は変更できません。

// 簡潔なレコード宣言
public record Person(string Name, int Age);

// 使用例
var person = new Person("田中", 30);
プロ太

これらはnullチェックが主目的ではないですが、「オブジェクトを正しく初期化しその後は変更しない」という原則を実現しやすくする機能です。

一度非nullの値で初期化したプロパティが後からnullに変更されるリスクを排除している点で、間接的にnull安全性の向上にも貢献していますね。

Visual StudioやAIによるnullチェック支援

Visual Studioなどの開発環境では、null安全性のための適切なコード修正を提案するための機能を強化しています。少し例をみてみましょう。

例えば、以下のコードでは、

internal class Program
{
    static void Main(string[] args)
    {
        var person = new Person { Name = "田中" }; //ここで警告発生
    }
}

internal class Person
{
    public string Name { get; set; } //null非許容型
}

以下の警告がでますね。

ここで、警告がでている箇所へカーソルをあてて「考えられる修正内容を表示する」を選択しましょう。

以下のように「プロパティを’required’にする」、「Null許容型として宣言する」などいくつか修正案が自動でててきました。

プロ美

修正方法をおしえてくれるんだ!これは便利!

プロ太

「Copilotで修正する」を選ぶと、AIに修正提案してもらうことも可能です!

Visual StudioにおけるGitHub Copilotの活用については、こちらも参考にしてください。

プログラミング初心者におすすめのAIツール ~学習を効率化する方法~【ChatGPT、GitHub Copilot(Visual Studio連携)】 プログラミングにおいてChatGPTやGitHub CopilotなどのAIを活用することで、開発効率を大幅に向上でき、学習者にとって...

まとめ

本記事では、C#におけるnullの概念とその安全な扱い方について解説しました。

nullは「10億ドルの過ち」と呼ばれるように、バグの原因になりやすい概念ですが、C#は長い歴史の中でnullとの戦いを続けてきました。

C#のnull対策の進化は以下の5つのフェーズに分けられます。

  • Phase①(2002-2004):手動nullチェックによる対策
  • Phase②(2005-2014):null合体演算子(??)とNullable<T>の導入
  • Phase③(2015-2018):null条件演算子(?.)によるコード簡略化
  • Phase④(2019):null許容参照型による革命的なビルド時チェック
  • Phase⑤(2020-2023):required修飾子やinit-onlyプロパティによる初期化安全性の向上

特にC#8.0で導入されたnull許容参照型は、コンパイル時にnull参照の危険性を検出できる革命的な機能で、モダンC#開発において重要な要素となっています。

Visual StudioやGitHub Copilotなどの開発ツールも、適切なnullチェックコードの提案など、null安全なコーディングをサポートしています。

これからのC#開発では、null許容型を積極的に活用して、より安全なコードを書いていきましょう。

プロ太

引き続き、一緒にC#プログラミングを楽しく学んでいきましょう。

ABOUT ME
プロ太
●仕事:現在は個人事業主(メンター・情報発信等)、大手IT企業で技術者・マネージャ(15年以上)、大学の外部講師、学生時代は学習塾で非常勤講師(約4年間) ●博士(工学)の学位取得 ●高校生の頃に独学で始め、プログラミング歴20年以上 ●言語:C# 、Java、C/C++、Python、JavaScript/TypeScript等 ●分野:Webアプリ、テスト自動化、生成AI、デバッガ、コード解析、ドメイン特化言語

ご依頼・ご相談について

プログラミング学習のご相談、お仕事のご依頼については、
こちらのお問い合わせページをご確認ください。