C# null許容型とは?使い方・「?」「??」の違いとNullReferenceException対策を初心者向けに解説

はじめに

C#でプログラムを書いていると、避けて通れないのがnullの扱いです。

特に初心者のうちは、

C#
NullReferenceException

というエラーに悩まされることがよくあります。

このエラーは、簡単にいうと「値が存在しないものに対して、プロパティやメソッドを使おうとした」ときに発生します。

そこで重要になるのが、C#のnull許容型です。

C#では、値が入っている場合と、値が存在しないnullの場合を区別するために、int?string?のような書き方を使います。

この記事では、C#のnull許容型について、初心者向けに以下の内容をわかりやすく解説します。

  • nullとは何か

  • int?string?の意味

  • ????.!の違い

  • NullReferenceExceptionが起きる原因

  • null関連のエラーを防ぐ実践的な書き方

C#のnull許容型を理解すると、エラーに強く、読みやすいコードを書けるようになります。

1. C#のnull許容型とは?初心者がまず押さえる基本

1-1. nullとは何か:値が存在しない状態を表す

nullとは、値が存在しない状態を表す特別な値です。

たとえば、次のような変数を考えてみます。

C#
string name = null;

この場合、nameには文字列が入っていません。

空文字の""とは意味が違います。

C#
string empty = "";
string nothing = null;

emptyは「空の文字列」という値があります。

一方、nothingは「文字列そのものが存在しない」状態です。

この違いはC#では非常に重要です。

1-2. nullが原因で起きる代表的なエラー

nullが原因でよく起きるエラーが、NullReferenceExceptionです。

たとえば、次のコードを見てください。

C#
string name = null;

Console.WriteLine(name.Length);

namenullなので、文字列の長さを表すLengthにアクセスできません。

そのため、実行時にNullReferenceExceptionが発生します。

これはC#初心者が特につまずきやすいポイントです。

1-3. null許容型は「nullを入れてよい型」を明示する仕組み

C#のnull許容型とは、その変数にnullを入れてよいかどうかを明示する仕組みです。

たとえば、次のように書きます。

C#
int? age = null;
string? name = null;

int?は「int型だが、nullも許可する」という意味です。

string?は「string型だが、nullになる可能性がある」という意味です。

このように?を使うことで、値が存在しない可能性をコード上で表現できます。

1-4. C#でnull許容型が重要な理由

C#でnull許容型が重要な理由は、主に次の3つです。

1つ目は、NullReferenceExceptionを防ぎやすくなることです。

2つ目は、変数や戻り値がnullになる可能性をコードから読み取れることです。

3つ目は、コンパイラがnullの危険性を警告してくれることです。

たとえば、string?と書いておけば、その変数はnullかもしれないと判断できます。

C#
string? name = GetName();

この時点で、nameを使う前にnullチェックが必要だとわかります。

1-5. この記事で学べること

この記事では、C#のnull許容型について、基礎から実践まで順番に解説します。

特に、次のような疑問を解消できます。

  • int?とは何か

  • string?stringは何が違うのか

  • ???はどう違うのか

  • ?.はどんなときに使うのか

  • !を使っても安全なのか

  • NullReferenceExceptionをどう防ぐのか

初心者でも理解しやすいように、できるだけ具体的なコード例を使って説明します。

2. C#における値型・参照型・nullの関係

2-1. 値型と参照型の違い

C#の型は、大きく分けると値型参照型があります。

値型の代表例は次のようなものです。

C#
int number = 10;
bool isActive = true;
DateTime today = DateTime.Now;

値型は、変数そのものが値を持ちます。

一方、参照型の代表例は次のようなものです。

C#
string name = "Taro";
Person person = new Person();
List<string> names = new List<string>();

参照型は、実際のオブジェクトへの参照を持ちます。

この違いが、nullの扱いに関係します。

2-2. intやboolなどの値型は通常nullを持てない

intboolなどの値型は、通常はnullを持てません。

たとえば、次のコードはコンパイルエラーになります。

C#
int age = null;
bool isActive = null;

intには整数値が必要で、boolにはtrueまたはfalseが必要です。

しかし実際の開発では、「年齢が未入力」「真偽値が未設定」のように、値がまだ存在しない状態を表したいことがあります。

そのようなときに使うのが、null許容値型です。

C#
int? age = null;
bool? isActive = null;

2-3. stringやクラスなどの参照型はnullになり得る

stringや自作クラスなどの参照型は、もともとnullになる可能性があります。

C#
string name = null;
Person person = null;

ただし、C# 8.0以降では、null許容参照型を有効にすることで、stringstring?を区別できるようになりました。

C#
string name = "Taro";   // nullを想定しない
string? nickname = null; // nullを許容する

この仕組みにより、参照型でも「nullになるかもしれない」という意図を明示できます。

2-4. C# 8.0以降で変わったnullの扱い

C# 8.0以降では、null許容参照型という機能が導入されました。

それ以前のC#では、参照型は常にnullになる可能性がありました。

しかしC# 8.0以降では、設定を有効にすると、次のように区別できます。

C#
string name = "Taro";     // 基本的にnullを入れない
string? nickname = null; // nullを入れてもよい

この機能を有効にすると、コンパイラがnullの危険性を警告してくれます。

たとえば、string?の変数をnullチェックせずに使うと、警告が出ることがあります。

C#
string? name = null;

Console.WriteLine(name.Length); // 警告

2-5. null許容値型とnull許容参照型の違い

C#のnull許容型には、大きく分けて2種類あります。

1つ目は、null許容値型です。

C#
int? age = null;
DateTime? birthday = null;
bool? isMember = null;

これはNullable<T>という仕組みを使っています。

2つ目は、null許容参照型です。

C#
string? name = null;
Person? person = null;

こちらは、主にコンパイラの静的解析によってnullの可能性をチェックする仕組みです。

重要なのは、int?string?では内部的な意味が少し違うという点です。

int?は実際にNullable<int>という型になります。

一方、string?は実行時に別の型になるわけではなく、コンパイラに「nullの可能性がある」と伝えるための情報です。

3. null許容値型の使い方:int?・DateTime?・bool?の基本

3-1. T?はNullable<T>の省略形

値型に対して?を付けると、null許容値型になります。

たとえば、次の2つは同じ意味です。

C#
int? age = null;
Nullable<int> age2 = null;

つまり、T?Nullable<T>の省略形です。

よく使われる例は次のとおりです。

C#
int? score = null;
DateTime? birthday = null;
bool? isDeleted = null;
decimal? price = null;

3-2. int?にnullを代入する書き方

int?には、整数値もnullも代入できます。

C#
int? age = 20;
age = null;

通常のintではnullを代入できません。

C#
int age = null; // エラー

しかしint?であれば、値が存在しない状態を表現できます。

たとえば、ユーザーが年齢を入力しなかった場合などに便利です。

C#
int? userAge = null;

3-3. HasValueで値があるか確認する

null許容値型では、HasValueを使うと値が入っているか確認できます。

C#
int? age = 25;

if (age.HasValue)
{
Console.WriteLine($"年齢は{age.Value}歳です");
}
else
{
Console.WriteLine("年齢は未設定です");
}

HasValuetrueなら値があります。

falseならnullです。

3-4. Valueで値を取り出すときの注意点

Valueを使うと、null許容値型から実際の値を取り出せます。

C#
int? age = 25;
int actualAge = age.Value;

ただし、nullの状態でValueにアクセスすると例外が発生します。

C#
int? age = null;

Console.WriteLine(age.Value); // 例外

そのため、Valueを使う前には必ずHasValueで確認するのが基本です。

C#
if (age.HasValue)
{
Console.WriteLine(age.Value);
}

または、後述する??GetValueOrDefaultを使うと安全に値を扱えます。

3-5. GetValueOrDefaultで既定値を取得する

GetValueOrDefaultを使うと、値がある場合はその値を、nullの場合は既定値を取得できます。

C#
int? age = null;

int result = age.GetValueOrDefault();

Console.WriteLine(result); // 0

intの既定値は0なので、nullの場合は0が返ります。

任意の既定値を指定することもできます。

C#
int? age = null;

int result = age.GetValueOrDefault(18);

Console.WriteLine(result); // 18

このように、nullの場合に代わりの値を使いたいときに便利です。

3-6. null許容値型がよく使われる場面

null許容値型は、次のような場面でよく使われます。

データベースのNULLを扱う場合です。

C#
DateTime? deletedAt = null;

未入力の数値を扱う場合です。

C#
int? age = null;

未確定の真偽値を扱う場合です。

C#
bool? isApproved = null;

たとえばbool?では、次の3つの状態を表現できます。

C#
true   // はい
false // いいえ
null // 未設定

通常のboolではtruefalseしか表せません。

そのため、「未選択」や「未回答」を表したい場合にbool?が役立ちます。

4. null許容参照型の使い方:string?・クラス?の基本

4-1. stringとstring?の違い

C# 8.0以降でnull許容参照型を有効にすると、stringstring?には意味の違いが生まれます。

C#
string name = "Taro";
string? nickname = null;

stringは、基本的にnullを想定しない文字列です。

string?は、nullになる可能性がある文字列です。

たとえば、次のように使い分けます。

C#
public class User
{
public string Name { get; set; } = "";
public string? Nickname { get; set; }
}

Nameは必ず必要な値、Nicknameは任意の値として表現できます。

4-2. null許容参照型を有効にする方法

null許容参照型は、プロジェクト設定またはファイル内のディレクティブで有効にできます。

プロジェクト全体で有効にする場合は、.csprojに次のように書きます。

XML
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>

これにより、そのプロジェクト内でnull許容参照型の警告が有効になります。

4-3. nullable enableの設定方法

ファイル単位で有効にしたい場合は、C#ファイルの先頭に次のように書きます。

C#
#nullable enable

例は次のとおりです。

C#
#nullable enable

string name = "Taro";
string? nickname = null;

一部のファイルだけ試験的に導入したい場合は、この方法が便利です。

反対に、特定のファイルで無効にしたい場合は次のように書きます。

C#
#nullable disable

4-4. null非許容参照型で警告が出るケース

null許容参照型を有効にしている場合、stringnullを代入しようとすると警告が出ます。

C#
string name = null; // 警告

また、string?をnullチェックせずに使う場合も警告が出ます。

C#
string? name = GetName();

Console.WriteLine(name.Length); // 警告

この警告は、「この変数はnullかもしれないので、そのまま使うと危険です」というサインです。

安全に使うには、次のようにnullチェックを行います。

C#
if (name != null)
{
Console.WriteLine(name.Length);
}

4-5. コンパイラ警告はエラーではなくバグ予防のサイン

null許容参照型の警告は、基本的にはコンパイルエラーではありません。

そのため、警告が出ていてもビルド自体は通ることがあります。

しかし、警告を無視すると、実行時にNullReferenceExceptionが発生する可能性があります。

たとえば、次のコードは危険です。

C#
string? name = null;

Console.WriteLine(name.Length);

コンパイラの警告は、実行前にバグの可能性を教えてくれる便利な仕組みです。

初心者のうちは、警告をできるだけ放置しないことが大切です。

4-6. 既存プロジェクトで導入するときの注意点

既存プロジェクトでnull許容参照型を有効にすると、大量の警告が出ることがあります。

これは、既存コードの多くがnullの可能性を明示していないためです。

いきなりプロジェクト全体で有効にすると修正範囲が広くなるため、次のように段階的に導入するのがおすすめです。

まず、新しく作るファイルだけ#nullable enableを使います。

次に、重要なクラスやバグが起きやすい部分から対応します。

最後に、プロジェクト全体で<Nullable>enable</Nullable>を設定します。

既存コードでは、無理にすべてを一度に直そうとせず、少しずつnull安全性を高めるのが現実的です。

5. 「?」「??」「?.」「!」の違いを整理

5-1. 型名の後ろの「?」:nullを許容する

型名の後ろに付ける?は、nullを許容することを表します。

C#
int? age = null;
string? name = null;
DateTime? birthday = null;

int?は、intまたはnullを表します。

string?は、stringまたはnullの可能性があることを表します。

つまり、型に付ける?は「この値はnullかもしれない」という意味です。

5-2. null合体演算子「??」:nullなら代替値を返す

??は、左側の値がnullなら右側の値を返す演算子です。

C#
string? name = null;

string displayName = name ?? "ゲスト";

Console.WriteLine(displayName); // ゲスト

namenullでなければ、その値が使われます。

C#
string? name = "Taro";

string displayName = name ?? "ゲスト";

Console.WriteLine(displayName); // Taro

??は、nullの場合のデフォルト値を設定したいときによく使います。

5-3. null合体代入演算子「??=」:nullなら代入する

??=は、変数がnullの場合だけ値を代入する演算子です。

C#
string? name = null;

name ??= "ゲスト";

Console.WriteLine(name); // ゲスト

すでに値が入っている場合は、代入されません。

C#
string? name = "Taro";

name ??= "ゲスト";

Console.WriteLine(name); // Taro

初期値が入っていない場合だけデフォルト値を設定したいときに便利です。

5-4. null条件演算子「?.」:nullなら処理を止める

?.は、左側がnullでなければメンバーにアクセスし、nullならその時点で処理を止めてnullを返します。

C#
string? name = null;

int? length = name?.Length;

Console.WriteLine(length); // null

通常の.を使うと、namenullの場合にNullReferenceExceptionが発生します。

C#
string? name = null;

int length = name.Length; // 危険

?.を使うことで、安全にメンバーへアクセスできます。

5-5. null免除演算子「!」:nullではないとコンパイラに伝える

!は、null免除演算子と呼ばれます。

これは、コンパイラに対して「この値はnullではありません」と伝えるための演算子です。

C#
string? name = GetName();

Console.WriteLine(name!.Length);

ただし、!は実行時にnullチェックをしてくれるわけではありません。

実際にnamenullだった場合は、NullReferenceExceptionが発生します。

C#
string? name = null;

Console.WriteLine(name!.Length); // 実行時に例外

そのため、!の使いすぎは危険です。

本当にnullではないと確信できる場合だけ使うべきです。

5-6. 「?」「??」「?.」「!」の使い分け早見表

????.!は似ていますが、役割はまったく違います。

書き方名前意味
int? / string?null許容型nullを許可するint? age = null;
??null合体演算子nullなら代替値を返すname ?? "ゲスト"
??=null合体代入演算子nullなら代入するname ??= "ゲスト"
?.null条件演算子nullならアクセスしないuser?.Name
!null免除演算子nullでないとコンパイラに伝えるname!.Length

初心者のうちは、次のように覚えるとわかりやすいです。

?は「nullかもしれない」。

??は「nullなら代わりにこれ」。

?.は「nullならそこで止まる」。

!は「nullではないと宣言する」。

6. NullReferenceExceptionとは?原因と発生しやすいコード

6-1. NullReferenceExceptionの意味

NullReferenceExceptionは、C#で非常によく見かける実行時エラーです。

意味は、nullの参照に対してメンバーへアクセスしようとしたということです。

たとえば、次のようなコードで発生します。

C#
string? name = null;

Console.WriteLine(name.Length);

namenullなので、Lengthにアクセスできません。

その結果、NullReferenceExceptionが発生します。

6-2. nullのオブジェクトにアクセスすると発生する

クラスのインスタンスがnullの場合も同じです。

C#
User? user = null;

Console.WriteLine(user.Name);

usernullなので、Nameにアクセスできません。

安全に書くには、次のようにnullチェックをします。

C#
if (user != null)
{
Console.WriteLine(user.Name);
}

または、?.を使います。

C#
Console.WriteLine(user?.Name);

6-3. 文字列・配列・List・クラスでよくある発生例

文字列での例です。

C#
string? message = null;

Console.WriteLine(message.ToUpper());

配列での例です。

C#
int[]? numbers = null;

Console.WriteLine(numbers.Length);

Listでの例です。

C#
List<string>? names = null;

names.Add("Taro");

クラスでの例です。

C#
User? user = null;

Console.WriteLine(user.Email);

いずれも、変数がnullの状態でメンバーにアクセスしているため、NullReferenceExceptionが発生します。

6-4. メソッドの戻り値がnullになるケース

メソッドの戻り値がnullになる場合も注意が必要です。

C#
User? FindUser(int id)
{
return null;
}

このメソッドを呼び出した側でnullチェックを忘れると危険です。

C#
User? user = FindUser(1);

Console.WriteLine(user.Name); // 危険

安全に書くなら、次のようにします。

C#
User? user = FindUser(1);

if (user != null)
{
Console.WriteLine(user.Name);
}
else
{
Console.WriteLine("ユーザーが見つかりませんでした");
}

6-5. 初期化忘れによるNullReferenceException

クラスのプロパティやフィールドの初期化忘れも、NullReferenceExceptionの原因になります。

C#
public class User
{
public string Name { get; set; }
}

null許容参照型が有効な場合、このコードでは警告が出ることがあります。

Nameが初期化されていないためです。

安全にするには、初期値を設定します。

C#
public class User
{
public string Name { get; set; } = "";
}

または、コンストラクタで必ず初期化します。

C#
public class User
{
public string Name { get; }

public User(string name)
{
Name = name;
}
}

6-6. エラーメッセージの読み方と原因の探し方

NullReferenceExceptionが発生したら、まず例外が出た行を確認します。

たとえば、次の行でエラーが出たとします。

C#
Console.WriteLine(user.Profile.Email);

この場合、nullの可能性があるのは複数あります。

C#
user
user.Profile
user.Profile.Email

特に問題になりやすいのは、途中のオブジェクトがnullになっているケースです。

安全に確認するには、次のように分解します。

C#
if (user == null)
{
Console.WriteLine("userがnullです");
}
else if (user.Profile == null)
{
Console.WriteLine("Profileがnullです");
}
else
{
Console.WriteLine(user.Profile.Email);
}

エラー行だけを見るのではなく、「どの変数がnullなのか」を順番に確認することが大切です。

7. NullReferenceExceptionを防ぐ実践的な対策

7-1. nullチェックを行う

最も基本的な対策は、nullチェックを行うことです。

C#
string? name = GetName();

if (name != null)
{
Console.WriteLine(name.Length);
}

name != nullで確認した後であれば、安全にLengthへアクセスできます。

7-2. if文で安全に分岐する

nullの場合とnullでない場合で処理を分けると、コードがわかりやすくなります。

C#
User? user = FindUser(1);

if (user == null)
{
Console.WriteLine("ユーザーが見つかりません");
}
else
{
Console.WriteLine(user.Name);
}

早期リターンを使う書き方もよく使われます。

C#
void PrintUserName(User? user)
{
if (user == null)
{
Console.WriteLine("ユーザーが存在しません");
return;
}

Console.WriteLine(user.Name);
}

この書き方は、nullの場合を先に処理できるため、後続のコードが読みやすくなります。

7-3. ??でデフォルト値を設定する

??を使うと、nullの場合の代替値を簡潔に書けます。

C#
string? name = null;

string displayName = name ?? "ゲスト";

Console.WriteLine(displayName);

数値でも使えます。

C#
int? age = null;

int displayAge = age ?? 0;

nullの場合に標準値を使いたい場面では、??が便利です。

7-4. ?.で安全にメンバーへアクセスする

?.を使うと、nullの可能性があるオブジェクトに安全にアクセスできます。

C#
User? user = null;

string? name = user?.Name;

usernullなら、user?.Nameの結果もnullになります。

メソッド呼び出しにも使えます。

C#
user?.SendEmail();

この場合、usernullならSendEmailは呼び出されません。

7-5. コンストラクタで必ず初期化する

nullを防ぐには、そもそも必要な値を必ず初期化する設計が重要です。

C#
public class User
{
public string Name { get; }

public User(string name)
{
Name = name;
}
}

このようにすれば、Userを作るときに必ずNameが設定されます。

C#
User user = new User("Taro");

必須項目はnull許容にせず、コンストラクタで受け取る設計にすると安全です。

7-6. nullを返さない設計にする

メソッドの戻り値で、できるだけnullを返さない設計にすることも大切です。

たとえば、該当するデータがない場合に、nullではなく空のリストを返す方法があります。

C#
List<string> GetNames()
{
return new List<string>();
}

呼び出し側はnullチェックをせずに使えます。

C#
foreach (var name in GetNames())
{
Console.WriteLine(name);
}

一方、検索結果が見つからないこと自体に意味がある場合は、User?のようにnull許容型で表現するのも自然です。

C#
User? FindUser(int id)
{
return null;
}

重要なのは、nullを返すかどうかの方針を明確にすることです。

7-7. nullable warningsを活用する

C# 8.0以降では、nullable warningsを活用することで、null関連のバグを事前に見つけやすくなります。

プロジェクトで有効にするには、.csprojに次のように書きます。

XML
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>

これにより、nullの可能性があるコードに警告が出るようになります。

たとえば、次のようなコードは警告の対象になります。

C#
string? name = null;

Console.WriteLine(name.Length);

警告が出たら、nullチェックや???.などを使って安全なコードに修正しましょう。

8. null許容型の具体的なコード例

8-1. int?を使ったサンプル

int?は、数値が未設定になる可能性がある場合に使います。

C#
int? age = null;

if (age.HasValue)
{
Console.WriteLine($"年齢は{age.Value}歳です");
}
else
{
Console.WriteLine("年齢は未設定です");
}

??を使うと、より短く書けます。

C#
int? age = null;

int displayAge = age ?? 0;

Console.WriteLine(displayAge);

8-2. DateTime?を使ったサンプル

DateTime?は、日付が未設定になる可能性がある場合によく使います。

C#
DateTime? deletedAt = null;

if (deletedAt == null)
{
Console.WriteLine("削除されていません");
}
else
{
Console.WriteLine($"削除日時: {deletedAt.Value}");
}

たとえば、論理削除の日時を表す場合に使われます。

C#
public class Article
{
public string Title { get; set; } = "";
public DateTime? DeletedAt { get; set; }
}

DeletedAtnullなら未削除、値があれば削除済みという意味にできます。

8-3. string?を使ったサンプル

string?は、文字列が存在しない可能性がある場合に使います。

C#
string? nickname = null;

string displayName = nickname ?? "名無し";

Console.WriteLine(displayName);

nullチェックをしてから使う例です。

C#
string? message = GetMessage();

if (message != null)
{
Console.WriteLine(message.ToUpper());
}

?.を使う例です。

C#
string? message = GetMessage();

int? length = message?.Length;

8-4. メソッドの引数にnull許容型を使う例

引数がnullになる可能性がある場合は、引数の型に?を付けます。

C#
void PrintName(string? name)
{
if (name == null)
{
Console.WriteLine("名前が指定されていません");
return;
}

Console.WriteLine(name);
}

呼び出し側は、文字列もnullも渡せます。

C#
PrintName("Taro");
PrintName(null);

nullを許可する引数では、メソッド内でnullチェックを行うことが重要です。

8-5. メソッドの戻り値にnull許容型を使う例

検索処理などでは、結果が見つからない可能性があります。

その場合、戻り値をnull許容型にできます。

C#
User? FindUserById(int id)
{
if (id == 1)
{
return new User("Taro");
}

return null;
}

呼び出し側では、戻り値がnullかどうか確認します。

C#
User? user = FindUserById(2);

if (user == null)
{
Console.WriteLine("ユーザーが見つかりません");
}
else
{
Console.WriteLine(user.Name);
}

8-6. nullチェック後に安全に値を使う例

nullチェックを行うと、その後の処理では安全に値を使えます。

C#
void PrintLength(string? text)
{
if (text == null)
{
Console.WriteLine("文字列がありません");
return;
}

Console.WriteLine(text.Length);
}

この例では、text == nullの場合に早期リターンしています。

そのため、後続のtext.Lengthでは、textがnullではないと判断できます。

値型の場合も同じです。

C#
void PrintScore(int? score)
{
if (!score.HasValue)
{
Console.WriteLine("スコアがありません");
return;
}

Console.WriteLine(score.Value);
}

ただし、int?の場合は??を使う方が簡潔な場合もあります。

C#
int? score = null;

Console.WriteLine(score ?? 0);

9. null許容型を使うべき場面・使わないほうがよい場面

9-1. データベースのNULLを扱う場合

データベースのカラムがNULLを許可している場合、C#側でもnull許容型を使うことがあります。

たとえば、次のようなカラムがあるとします。

  • AgeはNULL許可

  • DeletedAtはNULL許可

  • NicknameはNULL許可

C#では次のように表現できます。

C#
public class User
{
public int? Age { get; set; }
public DateTime? DeletedAt { get; set; }
public string? Nickname { get; set; }
}

データベースのNULLとC#のnullを対応させることで、データの状態を自然に表現できます。

9-2. 入力値が未設定になる可能性がある場合

ユーザー入力では、値が入力されないことがあります。

たとえば、任意入力の年齢やニックネームです。

C#
public class UserInput
{
public int? Age { get; set; }
public string? Nickname { get; set; }
}

未入力を表すために、nullを使うのは自然です。

ただし、必須入力の値にはnull許容型を使わない方がよいです。

C#
public class UserInput
{
public string Name { get; set; } = "";
}

必須項目は、nullではなく必ず値がある設計にしましょう。

9-3. 検索結果が見つからない可能性がある場合

検索処理では、結果が見つからないことがあります。

そのような場合、戻り値にnull許容型を使うことがあります。

C#
User? FindUser(string email)
{
// 見つからない場合はnullを返す
return null;
}

呼び出し側では、見つかった場合と見つからなかった場合を分けます。

C#
User? user = FindUser("test@example.com");

if (user == null)
{
Console.WriteLine("ユーザーが見つかりません");
}
else
{
Console.WriteLine(user.Name);
}

このように、存在しない可能性があるものには?を付けると意味が明確になります。

9-4. 何でもnull許容にすると危険な理由

null許容型は便利ですが、何でも?を付ければよいわけではありません。

たとえば、次のようなコードは危険です。

C#
public class User
{
public string? Name { get; set; }
public string? Email { get; set; }
public int? Age { get; set; }
}

すべてがnull許容になっていると、使う側は毎回nullチェックが必要になります。

C#
if (user.Name != null)
{
Console.WriteLine(user.Name);
}

本来必ず存在する値までnull許容にすると、コードが複雑になり、バグの原因になります。

必須の値はnull非許容にしましょう。

C#
public class User
{
public string Name { get; set; } = "";
public string Email { get; set; } = "";
public int? Age { get; set; }
}

9-5. nullではなく空文字・空配列を返すべきケース

場合によっては、nullではなく空文字や空配列、空リストを返した方がよいことがあります。

たとえば、一覧取得メソッドでは、データがない場合にnullではなく空リストを返す方が扱いやすいです。

C#
List<string> GetNames()
{
return new List<string>();
}

呼び出し側は、nullチェックなしでループできます。

C#
foreach (var name in GetNames())
{
Console.WriteLine(name);
}

文字列でも、値がないことを空文字で表した方が自然な場合があります。

C#
string GetDisplayName()
{
return "";
}

ただし、「未設定」と「空文字」を明確に区別したい場合はstring?を使う方が適切です。

9-6. OptionalやResult型を検討したほうがよいケース

より厳密に「値があるかないか」や「成功か失敗か」を表したい場合は、nullではなくOptionalResultのような設計を検討することもあります。

たとえば、処理が失敗する可能性がある場合に、単にnullを返すと失敗理由がわかりません。

C#
User? CreateUser(string name)
{
return null;
}

この場合、なぜ作成できなかったのかが呼び出し側に伝わりません。

そのような場合は、成功・失敗・エラーメッセージを含む結果型を使う設計もあります。

C#
public class Result<T>
{
public bool IsSuccess { get; set; }
public T? Value { get; set; }
public string ErrorMessage { get; set; } = "";
}

初心者のうちはまずnull許容型を理解すれば十分ですが、設計が複雑になる場合は、null以外の表現も検討するとよいでしょう。

10. 初心者が混乱しやすいポイントとよくある間違い

10-1. int?とintはそのまま同じように扱えない

int?intは似ていますが、同じようには扱えません。

C#
int? nullableAge = 20;
int age = nullableAge; // エラー

nullableAgenullの可能性があるため、そのままintに代入できません。

安全に代入するには、nullチェックを行います。

C#
int? nullableAge = 20;

if (nullableAge.HasValue)
{
int age = nullableAge.Value;
}

または、??でデフォルト値を指定します。

C#
int age = nullableAge ?? 0;

10-2. Valueを安易に使うと例外になる

int?などのnull許容値型では、Valueで値を取り出せます。

しかし、nullの状態でValueを使うと例外になります。

C#
int? score = null;

Console.WriteLine(score.Value); // 例外

安全に使うには、必ずHasValueを確認します。

C#
if (score.HasValue)
{
Console.WriteLine(score.Value);
}

または、??を使います。

C#
Console.WriteLine(score ?? 0);

初心者のうちは、Valueを直接使うよりも、??やnullチェックを使う方が安全です。

10-3. string?は実行時の型が変わるわけではない

string?は、実行時に特別な型になるわけではありません。

次の2つは、実行時にはどちらもstringです。

C#
string name = "Taro";
string? nickname = "T";

string?は、主にコンパイラに対して「この変数はnullかもしれない」と伝えるための情報です。

一方、int?Nullable<int>という別の型になります。

ここは、null許容値型とnull許容参照型の大きな違いです。

10-4. !を使いすぎるとnull安全性が下がる

!を使うと、コンパイラのnull警告を抑制できます。

C#
string? name = GetName();

Console.WriteLine(name!.Length);

しかし、これは「本当にnullではない」と保証するものではありません。

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

C#
string? name = null;

Console.WriteLine(name!.Length); // NullReferenceException

!は便利ですが、使いすぎるとnull許容参照型のメリットが薄れます。

基本はnullチェックや???.を使い、!は最後の手段として使うのがよいでしょう。

10-5. 警告を無視するとNullReferenceExceptionにつながる

null許容参照型を有効にすると、コンパイラがnullの危険性を警告してくれます。

たとえば、次のようなコードです。

C#
string? name = null;

Console.WriteLine(name.Length);

この警告を無視すると、実行時にNullReferenceExceptionが発生する可能性があります。

警告はエラーではないため放置しがちですが、実際にはバグの予兆です。

特に初心者は、警告を「あとで直すもの」ではなく、「今直すべきサイン」と考えるとよいでしょう。

10-6. nullチェック済みなのに警告が消えない原因

nullチェックをしているのに警告が消えないことがあります。

たとえば、複雑な条件式や別メソッドを経由している場合、コンパイラがnullでないことを判断できないことがあります。

C#
string? name = GetName();

if (IsValid(name))
{
Console.WriteLine(name.Length); // 警告が出る場合がある
}

IsValidの中でnullチェックをしていても、コンパイラがそれを十分に理解できない場合があります。

わかりやすく書くなら、直接nullチェックをします。

C#
string? name = GetName();

if (name != null)
{
Console.WriteLine(name.Length);
}

コンパイラにとっても人間にとっても、nullチェックが明確なコードを書くことが大切です。

11. null許容型に関するよくある質問

11-1. C#のnull許容型とは何ですか?

C#のnull許容型とは、変数にnullを入れられることを明示する型です。

値型では、int?DateTime?のように書きます。

C#
int? age = null;
DateTime? birthday = null;

参照型では、null許容参照型を有効にしたうえで、string?User?のように書きます。

C#
string? name = null;
User? user = null;

null許容型を使うことで、値が存在しない可能性をコード上で明確にできます。

11-2. int?とNullable<int>は同じですか?

はい、int?Nullable<int>は同じ意味です。

C#
int? age1 = null;
Nullable<int> age2 = null;

int?Nullable<int>の省略形です。

通常は、短くて読みやすいint?を使うことが多いです。

11-3. string?とstringの違いは何ですか?

null許容参照型が有効な場合、stringは基本的にnullを想定しない文字列、string?はnullになる可能性がある文字列を表します。

C#
string name = "Taro";
string? nickname = null;

string?を使うと、コンパイラが「この変数はnullかもしれない」と判断し、危険な使い方に警告を出してくれます。

ただし、string?は実行時に別の型になるわけではありません。

11-4. 「?」と「??」の違いは何ですか?

?は、型に付けてnullを許容するために使います。

C#
int? age = null;
string? name = null;

一方、??は、値がnullだった場合に代替値を返すために使います。

C#
string? name = null;

string displayName = name ?? "ゲスト";

つまり、?は型の指定、??は値を扱うときの演算子です。

11-5. null許容参照型は必ず有効にすべきですか?

新しいC#プロジェクトでは、null許容参照型を有効にするのがおすすめです。

nullの危険性をコンパイラが警告してくれるため、NullReferenceExceptionを防ぎやすくなります。

XML
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>

ただし、既存プロジェクトでは有効にした瞬間に大量の警告が出ることがあります。

その場合は、ファイル単位や機能単位で少しずつ導入するとよいでしょう。

11-6. NullReferenceExceptionを完全になくすことはできますか?

NullReferenceExceptionを完全になくすのは簡単ではありません。

外部API、データベース、古いコード、ライブラリなどからnullが入ってくる可能性があるためです。

ただし、次の対策を行うことで大幅に減らせます。

  • null許容型を正しく使う

  • nullチェックを行う

  • ??でデフォルト値を設定する

  • ?.で安全にアクセスする

  • 必須プロパティはコンストラクタで初期化する

  • nullable warningsを有効にする

  • 不要にnullを返さない設計にする

特に、C#のnull許容型とnullable warningsを組み合わせることで、実行前に多くのnull関連バグを発見できます。

まとめ

C#のnull許容型は、nullを安全に扱うための重要な仕組みです。

値型では、int?DateTime?bool?のように書くことで、通常はnullを持てない型にnullを許可できます。

C#
int? age = null;
DateTime? birthday = null;
bool? isActive = null;

参照型では、C# 8.0以降のnull許容参照型を使うことで、stringstring?を区別できます。

C#
string name = "Taro";
string? nickname = null;

また、nullを安全に扱うためには、演算子の違いを理解することも大切です。

?はnull許容型を表します。

??はnullの場合に代替値を返します。

??=はnullの場合だけ代入します。

?.はnullの場合に安全に処理を止めます。

!はnullではないとコンパイラに伝えますが、実行時の安全性を保証するものではありません。

NullReferenceExceptionを防ぐには、nullチェック、???.、コンストラクタでの初期化、nullable warningsの活用が効果的です。

C#のnull許容型を正しく使えるようになると、nullによるエラーを減らし、より安全で読みやすいコードを書けるようになります。