csharp nullableとは?null許容型・Nullable参照型・警告対策を初心者向けに徹底解説

はじめに

C#でアプリケーションを作っていると、初心者でも早い段階で null に出会います。たとえば、フォームに入力されていない値、データベース上で未設定の値、検索しても見つからなかったユーザー、まだ初期化されていないプロパティなどです。

このような「値が存在しない状態」を表すために使われるのが null です。しかし、null を正しく扱わないと、実行時に NullReferenceException が発生し、プログラムが落ちる原因になります。

そこで重要になるのが csharp nullable、つまりC#における nullable の仕組みです。C#には大きく分けて、値型に null を持たせる null許容型(Nullable<T>) と、参照型の null 安全性をコンパイラが警告してくれる Nullable参照型 があります。

この記事では、int?string?Nullable<T>???.!、nullable警告の意味、既存プロジェクトへの導入方法まで、初心者にもわかりやすく整理します。

1. csharp nullableとは?初心者が最初に押さえるべき全体像

1-1. nullableは「nullを入れられる型」を明示する仕組み

nullableとは、簡単に言うと「この変数には null が入る可能性があります」とコード上で明示する仕組みです。

C#では、次のように ? を付けることで nullable を表現します。

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

ただし、ここで重要なのは、int?string? は同じ意味ではないという点です。

int?値型である int に null を持たせる仕組み です。一方、string?参照型である string に null が入る可能性をコンパイラへ伝える注釈 です。

つまり、見た目はどちらも ? ですが、内部の仕組みは異なります。

1-2. C#でnullが問題になる理由:NullReferenceExceptionとは

C#で null が問題になる最大の理由は、null に対してプロパティやメソッドを呼び出すと、実行時に NullReferenceException が発生するからです。

C#
string? name = null;
Console.WriteLine(name.Length); // NullReferenceException の可能性

namenull の場合、Length プロパティを参照できません。なぜなら、null は「何も参照していない状態」だからです。

Nullable参照型は、このような NullReferenceException の可能性を減らすために、コンパイル時の静的解析で警告を出す仕組みです。Microsoft公式ドキュメントでも、Nullable参照型は NullReferenceException の可能性を最小化するためのコンパイル時機能として説明されています。

1-3. null許容型とNullable参照型は別物

csharp nullableを理解するときに、最初に混同しやすいのが次の2つです。

種類対象主な目的
null許容型int?DateTime?値型値型に null を持たせる
Nullable参照型string?User?参照型null の可能性をコンパイラに伝える

intboolDateTime などの値型は、通常 null を代入できません。

C#
int age = null; // コンパイルエラー

しかし、int? にすると null を代入できます。

C#
int? age = null; // OK

一方、string はもともと参照型なので、実行時には null を持つことができます。Nullable参照型を有効にした環境では、string は「nullを想定しない文字列」、string? は「nullを許可する文字列」という設計意図を表します。

1-4. 「int?」「string?」「Nullable<T>」の違いを整理

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

C#
int? a = 10;
Nullable<int> b = 10;

この2つは同じ意味です。

一方、string?Nullable<string> の省略形ではありません。Nullable<T> は値型に対して使う構造体であり、参照型である string には使えません。

C#
Nullable<int> number = 1;     // OK
Nullable<string> text = null; // エラー

Nullable参照型における string? は、あくまで「この参照はnullになる可能性がある」というコンパイラ向けの情報です。stringstring? は実行時の型としてはどちらも System.String で、違いは主にコンパイル時の警告に現れます。

1-5. csharp nullableを学ぶと何ができるようになるか

csharp nullableを理解すると、次のようなコードを書けるようになります。

C#
public string GetDisplayName(string? nickname, string userName)
{
return nickname ?? userName;
}

このコードでは、nicknamenull の場合に userName を返しています。nickname は未設定でもよい値、userName は必ず存在すべき値、という設計意図が型から読み取れます。

nullableを学ぶメリットは、単に警告を消せることではありません。コードを読む人に「どこでnullを許可しているのか」「どこではnullを許可していないのか」を明確に伝えられることです。

2. null許容型(Nullable<T>)とは?値型にnullを持たせる仕組み

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

C#の型は、大きく 値型参照型 に分かれます。

値型の代表例は次のとおりです。

C#
int
bool
double
decimal
DateTime
Guid
enum
struct

参照型の代表例は次のとおりです。

C#
string
object
class
array
interface
delegate

値型は通常、値そのものを保持します。そのため、int には整数、bool には true または falseDateTime には日時が入ります。

C#
int count = 0;
bool isActive = false;
DateTime createdAt = DateTime.Now;

これらの値型には、通常 null を代入できません。そこで、値型でも「値がない状態」を表したいときに使うのが Nullable<T> です。

2-2. int?・bool?・DateTime?の基本的な使い方

T? と書くと、値型 Tnull を持たせることができます。

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

たとえば、会員登録フォームで年齢が任意入力の場合、int よりも int? が自然です。

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

Agenull の場合は「未入力」、0 の場合は「0歳」という意味になります。0未入力 を区別できるのが int? の大きな利点です。

bool? もよく使われます。

C#
bool? answer = null;

この場合、truefalsenull の3状態を表せます。たとえばアンケートで「はい」「いいえ」「未回答」を区別したい場合に便利です。

2-3. Nullable<T>とT?の関係

T?Nullable<T> の省略記法です。

C#
int? score1 = 100;
Nullable<int> score2 = 100;

上記の2つは同じ意味です。

ただし、Nullable<T>T には値型しか指定できません。intboolDateTime などは指定できますが、string のような参照型は指定できません。

C#
Nullable<int> ok = 1;
// Nullable<string> ng = null; // 参照型なので不可

Microsoft公式ドキュメントでも、null許容値型 T? は、基になる値型 T のすべての値に加えて null を表せる型として説明されています。

2-4. HasValue・Value・GetValueOrDefaultの使い方

Nullable<T> には、値が入っているかどうかを確認するためのプロパティやメソッドがあります。

代表的なのは次の3つです。

メンバー意味
HasValue値がある場合は true
Value実際の値を取得する
GetValueOrDefault()値があればその値、なければ既定値を返す

コードで見てみましょう。

C#
int? score = 80;

if (score.HasValue)
{
Console.WriteLine(score.Value);
}
else
{
Console.WriteLine("点数は未入力です");
}

score.HasValuetrue の場合だけ score.Value を読むのが安全です。

GetValueOrDefault() を使うと、null の場合に値型の既定値を返せます。

C#
int? score = null;

int value = score.GetValueOrDefault();

Console.WriteLine(value); // 0

既定値を自分で指定することもできます。

C#
int? score = null;

int value = score.GetValueOrDefault(-1);

Console.WriteLine(value); // -1

ValueHasValuefalse のときにアクセスすると例外になります。公式ドキュメントでも、HasValuefalse のときに Value を読むと InvalidOperationException がスローされると説明されています。

2-5. null許容型でよくあるエラーと注意点

null許容型でよくあるミスは、null の可能性を確認せずに値を取り出すことです。

C#
int? age = null;
int actualAge = age.Value; // 危険

このコードはコンパイルできても、実行時に例外になる可能性があります。

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

C#
int? age = null;

if (age.HasValue)
{
int actualAge = age.Value;
Console.WriteLine(actualAge);
}
else
{
Console.WriteLine("年齢は未入力です");
}

または、?? 演算子を使います。

C#
int? age = null;

int actualAge = age ?? 0;

この場合、agenull なら 0 が使われます。

2-6. null許容型を使うべき場面:DB・フォーム・未入力データ

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

場面適した型
データベースのNULL退会日が未設定DateTime?
入力フォーム年齢が任意入力int?
アンケート回答が未選択bool?
検索条件最小価格が未指定decimal?
APIレスポンス値が返らない場合があるint?DateTime?

たとえば、退会日を表す DateTime は、退会していないユーザーの場合に値が存在しません。

C#
public class User
{
public DateTime? DeletedAt { get; set; }
}

DeletedAt == null なら未退会、値が入っていれば退会済み、という意味を表せます。

3. Nullable参照型とは?C# 8.0以降のnull安全機能

3-1. Nullable参照型の目的はnullを完全になくすことではない

Nullable参照型は、null を完全に禁止する機能ではありません。目的は、null を許可する場所と許可しない場所を明確にし、危険な使い方をコンパイラに警告してもらうことです。

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

string は「基本的にnullではないはず」、string? は「nullになる可能性がある」という意味になります。

Nullable参照型は実行時の型を変える機能ではありません。コンパイラが静的解析を行い、危険なコードに警告を出す仕組みです。

3-2. stringとstring?の違い

Nullable参照型が有効な環境では、stringstring? は次のように使い分けます。

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

name は必ず値が入る前提です。

C#
Console.WriteLine(name.Length); // 通常は警告なし

一方、nicknamenull の可能性があるため、そのまま使うと警告が出ます。

C#
Console.WriteLine(nickname.Length); // 警告の可能性

安全に使うには、nullチェックをします。

C#
if (nickname is not null)
{
Console.WriteLine(nickname.Length);
}

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

C#
Console.WriteLine(nickname?.Length ?? 0);

3-3. 非null参照型とnull許容参照型の考え方

Nullable参照型を有効にすると、参照型は大きく2種類に分かれます。

種類意味
非null参照型stringnullを想定しない
null許容参照型string?nullを許可する

たとえば、次のようなメソッドがあるとします。

C#
public void SendEmail(string email)
{
Console.WriteLine(email.ToLower());
}

このメソッドの emailstring なので、呼び出し側は null ではない文字列を渡すべきです。

C#
SendEmail("user@example.com");

一方、次のように string? にした場合は、メソッド内で null を考慮する必要があります。

C#
public void SendEmail(string? email)
{
if (email is null)
{
return;
}

Console.WriteLine(email.ToLower());
}

? を付けるということは、「この値がnullでも正常な入力として扱う」という設計判断です。

3-4. Nullable参照型はコンパイル時の警告機能

Nullable参照型は、実行時に自動でnullチェックを追加してくれる機能ではありません。

次のコードを見てください。

C#
string? name = null;
Console.WriteLine(name.Length);

Nullable参照型が有効なら、コンパイラは「nullの可能性がある参照を逆参照している」と警告します。しかし、警告を無視して実行すれば、実行時に NullReferenceException が発生する可能性があります。

つまり、Nullable参照型は「安全にしてくれる魔法」ではなく、「危険な場所を教えてくれる仕組み」です。

3-5. Nullable参照型を有効化する方法

プロジェクト全体でNullable参照型を有効にするには、.csproj に次の設定を追加します。

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

この設定により、参照型に対するnullable注釈と警告が有効になります。Microsoft公式ドキュメントでは、.csproj<Nullable> 要素で nullable annotation context と nullable warning context を設定できると説明されています。

ファイル単位で有効にすることもできます。

C#
#nullable enable

public class Sample
{
public string Name { get; set; } = "";
public string? Description { get; set; }
}

一部のファイルだけ試したい場合や、既存プロジェクトへ段階的に導入したい場合は、#nullable enable が便利です。

3-6. .csprojのNullable設定と#nullable enableの使い分け

基本的には、新規プロジェクトや積極的に改善しているプロジェクトでは .csproj に設定するのがおすすめです。

XML
<Nullable>enable</Nullable>

一方、古いプロジェクトでは、いきなり全体で有効化すると警告が大量に出ることがあります。その場合は、ファイル単位で次のように始めるとよいでしょう。

C#
#nullable enable

Nullable設定には、主に次の値があります。

設定意味
disablenullable注釈・警告を無効化
enablenullable注釈・警告を有効化
warnings警告のみ有効化
annotations注釈のみ有効化

既存コードを移行する場合、公式ドキュメントでも、ファイル単位・セクション単位で段階的に警告を出しながら最終的に <Nullable>enable</Nullable> へ収束させる考え方が紹介されています。

4. C# nullableの基本構文とよく使う演算子

4-1. ?演算子:nullを許可する

nullableで最もよく見る記号が ? です。

値型に対して使う場合は、null許容型を表します。

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

参照型に対して使う場合は、null許容参照型を表します。

C#
string? nickname = null;
User? user = FindUser(id);

ただし、int?string? の仕組みは異なります。int?Nullable<int>string? はコンパイラへのnull許容注釈です。

4-2. ??演算子:nullの場合のデフォルト値を指定する

?? は null合体演算子と呼ばれ、左辺が null でなければ左辺を返し、null なら右辺を返します。

C#
string? name = null;

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

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

null許容型にも使えます。

C#
int? age = null;

int displayAge = age ?? 0;

?? は、nullableを扱うコードで非常によく使う演算子です。C#のnull演算子に関する公式ドキュメントでも、?? は左辺が非nullなら左辺、nullなら右辺を返す演算子として説明されています。

4-3. ??=演算子:nullのときだけ代入する

??= は、左辺が null のときだけ右辺を代入する演算子です。

C#
List<string>? items = null;

items ??= new List<string>();

items.Add("C#");

通常の if で書くと、次のようになります。

C#
if (items is null)
{
items = new List<string>();
}

??= を使うと、遅延初期化のコードを短く書けます。

C#
private List<string>? _cache;

public List<string> GetCache()
{
_cache ??= LoadCache();
return _cache;
}

private List<string> LoadCache()
{
return new List<string> { "A", "B", "C" };
}

4-4. ?.演算子:nullなら処理をスキップする

?. は null条件演算子です。左辺が null でなければメンバーにアクセスし、null なら式全体が null になります。

C#
User? user = null;

int? nameLength = user?.Name.Length;

通常なら、user.Name にアクセスした時点で NullReferenceException になる可能性があります。しかし user?.Name と書くことで、usernull の場合はそこで処理が止まります。

ネストしたオブジェクトにも便利です。

C#
string? city = order?.Customer?.Address?.City;

公式ドキュメントでも、?. は左辺が非nullの場合だけメンバーへアクセスし、nullの場合は NullReferenceException を投げずに式全体を null と評価すると説明されています。

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

! は null許容の文脈では null許容警告を抑制する演算子 として使われます。null-forgiving operator、または null抑制演算子と呼ばれます。

C#
string? name = GetName();

Console.WriteLine(name!.Length);

このコードは、「namestring? だけれど、ここでは絶対にnullではないと開発者が判断している」とコンパイラへ伝えます。

ただし、! は実行時のnullチェックを追加しません。実際に namenull なら、普通に NullReferenceException が発生します。

C#
string? name = null;

Console.WriteLine(name!.Length); // 実行時エラー

! は便利ですが、使いすぎるとnullableの意味がなくなります。

4-6. is not nullを使った安全なnullチェック

nullableを扱うときは、is not null を使ったnullチェックが読みやすく安全です。

C#
string? name = GetName();

if (name is not null)
{
Console.WriteLine(name.Length);
}

if ブロックの中では、コンパイラが name はnullではないと判断し、警告が消えます。

is null も使えます。

C#
if (name is null)
{
Console.WriteLine("名前がありません");
}

== null でも多くの場合は動きますが、型によっては == 演算子がオーバーロードされている場合があります。is null / is not null は実際のnull参照を判定するため、nullチェックとして明確です。

4-7. nullチェックの書き方をコード例で比較

同じ処理を複数の書き方で比較してみましょう。

C#
string? name = GetName();

昔ながらの if 文です。

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

is not null を使う書き方です。

C#
if (name is not null)
{
Console.WriteLine(name.Length);
}

?.?? を使う書き方です。

C#
int length = name?.Length ?? 0;

早期returnを使う書き方です。

C#
if (name is null)
{
return;
}

Console.WriteLine(name.Length);

実務では、早期returnと is null / is not null を組み合わせると、ネストが浅くなり読みやすくなります。

C#
public void PrintName(string? name)
{
if (name is null)
{
Console.WriteLine("名前がありません");
return;
}

Console.WriteLine(name.Length);
}

5. nullable警告の意味と対策

5-1. nullable警告が出る理由

Nullable参照型を有効にすると、コンパイラは「このコードはnullで落ちるかもしれない」と判断した場所に警告を出します。

代表的な警告には、次のようなものがあります。

警告コード意味
CS8600nullの可能性がある値を非null型に変換している
CS8602nullの可能性がある参照を逆参照している
CS8618非nullプロパティがコンストラクタ終了時に初期化されていない
CS8625非null参照型にnullを代入している

これらはエラーではなく警告です。しかし、無視すると実行時の NullReferenceException につながる可能性があります。

Microsoft公式ドキュメントでは、nullable警告はコンパイラの静的解析により、null参照例外につながる可能性のあるコードに対して出されると説明されています。

5-2. CS8600:nullリテラルまたはnullの可能性を変換できない

CS8600は、null の可能性がある値を、nullを許可しない型に代入しようとしたときに出ます。

C#
string? nullableName = GetName();

string name = nullableName; // CS8600 の可能性

nullableNamenull の可能性があるのに、namestring なのでnullを想定していません。

対策は主に3つあります。

1つ目は、代入先も string? にする方法です。

C#
string? name = nullableName;

2つ目は、nullの場合の代替値を指定する方法です。

C#
string name = nullableName ?? "";

3つ目は、nullチェックしてから代入する方法です。

C#
if (nullableName is not null)
{
string name = nullableName;
}

5-3. CS8602:nullの可能性がある参照の逆参照

CS8602は、nullable警告の中でも特によく出ます。

C#
string? name = GetName();

Console.WriteLine(name.Length); // CS8602

namenull の可能性があるのに、Length を呼び出しているため警告されます。

対策は、nullチェックです。

C#
if (name is not null)
{
Console.WriteLine(name.Length);
}

または ?. を使います。

C#
Console.WriteLine(name?.Length);

null の場合にデフォルト値を使うなら、?? と組み合わせます。

C#
Console.WriteLine(name?.Length ?? 0);

公式ドキュメントでも、nullable変数の逆参照に関する警告への対処として、nullチェック、null解析属性、null-forgiving演算子の利用が挙げられています。

5-4. CS8618:非nullプロパティが初期化されていない

CS8618は、クラスの非nullプロパティがコンストラクタ終了時点で初期化されていない場合に出ます。

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

Namestring なのでnullを許可していません。しかし、インスタンス作成直後は Name に値が入っていないため、警告が出ます。

対策1:初期値を入れる。

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

対策2:コンストラクタで初期化する。

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

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

対策3:C# 11以降なら required を使う。

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

required は、オブジェクト初期化時にそのメンバーを必ず設定させるための修飾子です。公式ドキュメントでも、required はフィールドやプロパティがオブジェクト初期化子で初期化される必要があることを示すと説明されています。

5-5. CS8625:非null参照型にnullを代入している

CS8625は、非null参照型に null を代入したときに出ます。

C#
string name = null; // CS8625

namestring なのでnullを許可しません。

nullを許可したいなら string? にします。

C#
string? name = null;

nullを許可したくないなら、空文字や既定値を入れます。

C#
string name = "";

ただし、空文字とnullは意味が違います。空文字は「値はあるが文字数が0」、nullは「値そのものが存在しない」という意味です。

5-6. nullable警告を消す前に確認すべきこと

nullable警告が出たとき、すぐに ! を付けて消すのはおすすめしません。

まず確認すべきなのは、次の3点です。

確認事項考えること
その値は本当にnullになるのか外部入力、DB、API、検索結果を確認
nullの場合の正しい動作は何かエラーにする、既定値を使う、処理をスキップする
型の設計が正しいかstringstring? かを見直す

たとえば、ユーザー名が必須なら string? にするべきではありません。

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

一方、ニックネームが任意なら string? が自然です。

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

警告は邪魔なものではなく、設計のズレを教えてくれるサインです。

5-7. !で警告を抑制してよいケース・危険なケース

! を使ってよいケースは、コンパイラには判断できないが、開発者には確実にnullでないとわかる場合です。

たとえば、テストコードではよく使われます。

C#
User? user = repository.FindById(1);

Assert.NotNull(user);

Console.WriteLine(user!.Name);

この例では、Assert.NotNull(user) によってnullでないことを確認したつもりでも、コンパイラがそれを十分に理解できない場合があります。そのときに ! を使うことがあります。

一方、次のような使い方は危険です。

C#
string? name = GetName();

Console.WriteLine(name!.Length);

GetName() が本当にnullを返さない保証がないなら、! は警告を隠しているだけです。

安全なコードにするなら、次のように書きます。

C#
string? name = GetName();

if (name is null)
{
return;
}

Console.WriteLine(name.Length);

6. nullableを安全に使う実践パターン

6-1. メソッド引数でnullを許可する場合・しない場合

メソッド引数に ? を付けるかどうかは、そのメソッドが null を正常な入力として扱うかどうかで決めます。

nullを許可しない例です。

C#
public void RegisterUser(string email)
{
ArgumentNullException.ThrowIfNull(email);

Console.WriteLine(email.ToLower());
}

呼び出し側は、必ずメールアドレスを渡すべきです。

nullを許可する例です。

C#
public string GetDisplayName(string userName, string? nickname)
{
return nickname ?? userName;
}

この場合、nickname は未設定でも問題ありません。

重要なのは、「念のため ? を付ける」のではなく、「nullを許可する設計上の理由があるから ? を付ける」ことです。

6-2. 戻り値にnullを返す設計の注意点

戻り値に null を返す設計は、呼び出し側にnullチェックを求める設計です。

C#
public User? FindUser(int id)
{
return _users.FirstOrDefault(x => x.Id == id);
}

このメソッドは、ユーザーが見つからない場合に null を返します。

呼び出し側では、必ずnullチェックします。

C#
User? user = FindUser(1);

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

Console.WriteLine(user.Name);

一方、見つからないことが例外的な状況なら、例外を投げる設計もあります。

C#
public User GetUserOrThrow(int id)
{
return _users.FirstOrDefault(x => x.Id == id)
?? throw new InvalidOperationException("ユーザーが見つかりません");
}

null を返すか、例外を投げるか、空のコレクションを返すかは、API設計として一貫させることが重要です。

6-3. プロパティ初期化で警告を出さない書き方

非nullプロパティは、必ず初期化する必要があります。

もっとも簡単なのは、初期値を設定する方法です。

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

コンストラクタで初期化する方法もあります。

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

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

C# 11以降では required も使えます。

C#
public class Product
{
public required string Name { get; init; }
}

init を使うと、オブジェクト初期化時だけ値を設定でき、作成後の変更を防ぎやすくなります。公式ドキュメントでも、init はオブジェクト構築中だけプロパティへ値を設定できるアクセサとして説明されています。

6-4. コンストラクタで非nullを保証する

非nullプロパティがある場合は、コンストラクタで必ず値を受け取る設計がわかりやすいです。

C#
public class Customer
{
public string Name { get; }
public string Email { get; }

public Customer(string name, string email)
{
ArgumentNullException.ThrowIfNull(name);
ArgumentNullException.ThrowIfNull(email);

Name = name;
Email = email;
}
}

このようにすると、Customer オブジェクトが作成された時点で、NameEmail がnullではないことを保証できます。

さらに、空文字も禁止したい場合は、追加チェックを行います。

C#
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("名前は必須です", nameof(name));
}

nullableは「nullかどうか」を扱う機能であり、空文字や不正な値までは自動で検出しません。

6-5. ArgumentNullException.ThrowIfNullを使う

.NET 6以降では、ArgumentNullException.ThrowIfNull を使うと、引数のnullチェックを簡潔に書けます。

C#
public void Send(string email)
{
ArgumentNullException.ThrowIfNull(email);

Console.WriteLine(email.ToLower());
}

従来の書き方は次のようなものでした。

C#
if (email is null)
{
throw new ArgumentNullException(nameof(email));
}

ThrowIfNull を使うと、引数チェックが短くなります。

C#
public class MailService
{
public void Send(string to, string subject, string body)
{
ArgumentNullException.ThrowIfNull(to);
ArgumentNullException.ThrowIfNull(subject);
ArgumentNullException.ThrowIfNull(body);

// メール送信処理
}
}

publicメソッドやライブラリの入口では、nullable警告に頼るだけでなく、実行時の防御的チェックも重要です。

6-6. TryGetパターンとnullableの相性

C#では、値を取得できるかどうかを bool で返す TryGetパターンがよく使われます。

C#
public bool TryGetUser(int id, out User? user)
{
user = _users.FirstOrDefault(x => x.Id == id);
return user is not null;
}

呼び出し側です。

C#
if (TryGetUser(1, out User? user))
{
Console.WriteLine(user.Name);
}

ただし、このままだとコンパイラが user は非nullだと判断できない場合があります。その場合は、NotNullWhen 属性を使うと、より正確にnull状態を伝えられます。

C#
using System.Diagnostics.CodeAnalysis;

public bool TryGetUser(int id, [NotNullWhen(true)] out User? user)
{
user = _users.FirstOrDefault(x => x.Id == id);
return user is not null;
}

これにより、「戻り値が true のとき、user はnullではない」という契約をコンパイラへ伝えられます。

6-7. LINQ・FirstOrDefault・SingleOrDefaultでの注意点

LINQの FirstOrDefaultSingleOrDefault は、該当する要素がない場合に既定値を返します。

参照型の場合、既定値は null です。

C#
User? user = users.FirstOrDefault(x => x.Id == id);

そのため、戻り値は User? として扱うのが自然です。

C#
if (user is null)
{
Console.WriteLine("見つかりません");
return;
}

Console.WriteLine(user.Name);

FirstSingle は、該当する要素がない場合に例外を投げます。

C#
User user = users.First(x => x.Id == id);

どちらを使うべきかは、データが存在しないことを通常ケースとして扱うか、異常ケースとして扱うかで判断します。

7. null許容型とNullable参照型の違いを比較

7-1. 対象が値型か参照型か

null許容型とNullable参照型の最大の違いは、対象です。

null許容型は値型に対して使います。

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

Nullable参照型は参照型に対して使います。

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

int?Nullable<int> ですが、string?Nullable<string> ではありません。

7-2. 実行時の動作とコンパイル時の警告の違い

int? は実行時にも意味のある型です。Nullable<int> という構造体として扱われ、HasValueValue を持ちます。

C#
int? number = 10;

Console.WriteLine(number.HasValue);

一方、string? は実行時には通常の string と同じ参照型です。違いはコンパイル時の解析にあります。

C#
string? text = null;

string? は、コンパイラに「この変数はnullになる可能性がある」と伝える注釈です。

7-3. int?とstring?の意味は同じではない

初心者が最も混同しやすいポイントは、int?string? を同じ仕組みだと思ってしまうことです。

C#
int? number = null;
string? text = null;

見た目は似ていますが、意味は違います。

記法意味
int?Nullable<int> の省略形
string?nullを許可する参照型という注釈

int? は値型をラップする仕組みです。string? は静的解析のための情報です。

7-4. Nullable<T>にできる型・できない型

Nullable<T> に指定できるのは、値型だけです。

指定できる例です。

C#
Nullable<int>
Nullable<bool>
Nullable<DateTime>
Nullable<Guid>

指定できない例です。

C#
// Nullable<string>
// Nullable<object>
// Nullable<User>

参照型には Nullable<T> ではなく、Nullable参照型の ? を使います。

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

7-5. 初心者が混同しやすいポイント

よくある混同を整理します。

1つ目は、string? にすれば実行時エラーが起きないと思ってしまうことです。

C#
string? name = null;
Console.WriteLine(name.Length); // 危険

string? は警告を出すだけで、実行時のnullチェックは追加しません。

2つ目は、警告を消すために何でも ? を付けてしまうことです。

C#
public string? Name { get; set; }

Name が本来必須なら、string? にするのではなく、初期化方法を見直すべきです。

3つ目は、! を安全化の演算子だと思ってしまうことです。

C#
name!.Length

! は警告を抑制するだけで、nullを非nullに変換するわけではありません。

7-6. 比較表で理解するcsharp nullableの全体像

csharp nullableの全体像を表にすると、次のようになります。

項目null許容型Nullable参照型
int?DateTime?string?User?
対象値型参照型
実体Nullable<T>コンパイラ注釈
実行時の型変わる基本的に変わらない
主な目的値型で「値なし」を表すnullの危険を警告する
代表的な機能HasValueValuenullable警告、null状態解析
よく使う場面DBのNULL、任意入力API設計、null安全性の向上

この違いを理解すると、int?string? を正しく使い分けられるようになります。

8. 既存プロジェクトでNullable参照型を有効化する手順

8-1. いきなり全体で有効化しないほうがよい理由

既存プロジェクトでいきなり <Nullable>enable</Nullable> を設定すると、大量の警告が出ることがあります。

XML
<Nullable>enable</Nullable>

警告が多すぎると、どれから直せばよいかわからなくなり、結果的にすべて無視される可能性があります。

特に古いプロジェクトでは、次のようなコードが多く存在します。

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

Nullable参照型を有効にすると、Name が初期化されていないためCS8618が出ます。

既存プロジェクトでは、一気に全体を直すよりも、影響範囲を区切って進めるのが現実的です。

8-2. ファイル単位で#nullable enableを試す

まずは、1ファイルだけ #nullable enable を追加して試す方法があります。

C#
#nullable enable

public class UserService
{
public User? FindUser(int id)
{
return null;
}
}

この方法なら、他のファイルに大量の警告を出さずに、nullableの効果を確認できます。

サービスクラス、DTO、ViewModelなど、影響範囲が比較的わかりやすい場所から始めるとよいでしょう。

8-3. 警告を分類して優先順位をつける

警告が出たら、やみくもに修正するのではなく分類します。

分類優先度
実行時エラーにつながるCS8602
API設計に関わる引数・戻り値の ?
初期化不足CS8618
一時的な移行ノイズ古いDTOなど中〜低

特に優先したいのは、CS8602 のようなnull逆参照の警告です。これは実際の NullReferenceException に直結しやすいためです。

8-4. DTO・Entity・ViewModelでの対応方針

DTO、Entity、ViewModelでは、nullableの扱いを明確に分ける必要があります。

DTOでは、外部APIやJSONの値が欠ける可能性があります。

C#
public class UserDto
{
public string? Name { get; set; }
public string? Email { get; set; }
}

Entityでは、DBのNOT NULL制約と合わせることが重要です。

C#
public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
public DateTime? DeletedAt { get; set; }
}

ViewModelでは、画面入力の未入力状態を表すために string?int? を使うことがあります。

C#
public class SearchViewModel
{
public string? Keyword { get; set; }
public int? MinPrice { get; set; }
}

重要なのは、層ごとの役割に応じて「nullを許可する理由」を明確にすることです。

8-5. ライブラリや外部APIとのnull扱い

外部ライブラリや古いAPIは、nullable注釈が付いていない場合があります。その場合、コンパイラがnullの可能性を正確に判断できないことがあります。

たとえば、外部APIの戻り値が実際にはnullを返す可能性があるのに、型上は string になっている場合があります。

C#
string result = externalApi.GetName();

このような場合は、自分のコード側で境界を作り、nullチェックや変換を行うと安全です。

C#
string? rawName = externalApi.GetName();

if (string.IsNullOrWhiteSpace(rawName))
{
return "Unknown";
}

return rawName;

外部との境界では、相手のnullable情報を信用しすぎず、防御的に扱うことが大切です。

8-6. チーム開発でnullableルールを統一する

チーム開発では、nullableの使い方を統一しないとコードの意味がぶれます。

たとえば、次のようなルールを決めておくとよいでしょう。

ルール
nullを許可する場合だけ ? を付けるstring? Nickname
必須プロパティはコンストラクタか required で初期化するrequired string Name
! は原則使わない使用時は理由を書く
public APIのnullable注釈を重視する引数・戻り値を明確化
警告を放置しないCIで警告を管理する

nullableは個人の好みではなく、コードベース全体の設計ルールとして扱うと効果が高まります。

9. nullableでよくある疑問とトラブルシューティング

9-1. なぜstring?にしたのに実行時エラーが出るのか

string? は実行時エラーを防ぐ機能ではないからです。

C#
string? name = null;
Console.WriteLine(name.Length);

このコードは、namenull なら実行時に落ちます。Nullable参照型は、コンパイル時に警告を出してくれるだけです。

安全にするには、nullチェックが必要です。

C#
if (name is not null)
{
Console.WriteLine(name.Length);
}

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

C#
Console.WriteLine(name?.Length ?? 0);

9-2. nullableを有効にしたら警告だらけになったときの対処

既存プロジェクトで警告が大量に出た場合は、全てを一度に直そうとしないことが重要です。

まずは .csproj 全体で有効にするのではなく、ファイル単位で始めます。

C#
#nullable enable

または、移行方針として warningsannotations を使う方法もあります。

XML
<Nullable>warnings</Nullable>
XML
<Nullable>annotations</Nullable>

公式ドキュメントでも、既存コードベースをnullableへ移行する場合は、警告と注釈を段階的に扱う戦略が紹介されています。

9-3. !を使えば安全になるのか

安全にはなりません。

C#
string? name = null;

Console.WriteLine(name!.Length);

! はコンパイラの警告を消すだけです。実行時に namenull なら、NullReferenceException が発生します。

! を使う前に、まずはnullチェックできないか考えるべきです。

C#
if (name is null)
{
return;
}

Console.WriteLine(name.Length);

! は最後の手段です。

9-4. nullチェックしているのに警告が消えない理由

nullチェックしているのに警告が消えない場合、コンパイラがそのチェックを理解できていない可能性があります。

たとえば、独自メソッドでnullチェックしている場合です。

C#
if (IsValid(user))
{
Console.WriteLine(user.Name); // 警告が残る場合がある
}

IsValid の中で user is not null を確認していても、コンパイラが「true のとき user は非null」と判断できないことがあります。

この場合、属性を使ってコンパイラに伝えます。

C#
using System.Diagnostics.CodeAnalysis;

public bool IsValid([NotNullWhen(true)] User? user)
{
return user is not null;
}

または、直接 is not null を使うとわかりやすいです。

C#
if (user is not null)
{
Console.WriteLine(user.Name);
}

9-5. required・init・コンストラクタとの関係

非nullプロパティの初期化には、主に3つの方法があります。

1つ目は、初期値を設定する方法です。

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

2つ目は、コンストラクタで受け取る方法です。

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

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

3つ目は、requiredinit を使う方法です。

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

required は「作成時に必ず設定してほしい」、init は「初期化時だけ設定可能にしたい」という意図を表せます。

C#
var user = new User
{
Name = "Alice"
};

DTOや設定オブジェクトでは、requiredinit の組み合わせが読みやすい場合があります。

9-6. Entity Framework Coreでnullableを使うときの注意点

Entity Framework Coreでは、Nullable参照型の設定がDBスキーマの必須・任意の解釈に影響することがあります。

たとえば、次のようなエンティティを考えます。

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

Name は必須、Nickname は任意という意図が表れています。

既存のEF CoreプロジェクトでNullable参照型を有効にする場合は注意が必要です。公式ドキュメントでも、既存プロジェクトでNullable参照型を有効にすると、以前は任意として設定されていた参照型プロパティが、明示的にnullableと注釈されない限り必須として扱われ、マイグレーションで列のnull許容が変わる可能性があると説明されています。

そのため、EF Coreでは次の点を確認しましょう。

確認項目内容
DB列がNULL許可かstring? にするか判断
必須列かstringrequired、コンストラクタ初期化
マイグレーション差分意図しないNULL制約変更がないか確認
ナビゲーションプロパティ遅延読み込み・Includeの有無を考慮

nullableを有効にした直後のマイグレーションは、必ず慎重に確認するべきです。

9-7. Unityや古いC#環境でnullableは使えるのか

Nullable参照型はC# 8.0以降の機能です。そのため、使用できるかどうかは、プロジェクトが利用しているC#言語バージョンやビルド環境に依存します。

Unityの場合も、利用しているUnityのバージョンによって対応するC#コンパイラや言語バージョンが異なります。Unity 6系の公式マニュアルでは、対象バージョンのUnity EditorがRoslynコンパイラとC# 9.0を使用すると記載されています。

ただし、Unityでは .csproj が自動生成されるなど、通常の.NETプロジェクトとは設定方法が異なる場合があります。また、Unity特有のライフサイクル、たとえば AwakeStart で初期化されるフィールドは、コンパイラのnullable解析と相性が悪いことがあります。

古いC#環境では、string? などのNullable参照型が使えない場合があります。その場合でも、値型の int?DateTime? はC# 2.0から存在する機能なので、Nullable参照型とは分けて考える必要があります。

10. csharp nullableのベストプラクティス

10-1. nullを許可する理由がある場合だけ?を付ける

nullableを使うときの基本は、nullを許可する理由がある場合だけ ? を付けることです。

悪い例です。

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

NameEmail が必須なら、? を付けるべきではありません。

良い例です。

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

必須の値は非null、任意の値だけnullableにします。

10-2. 可能な限りnullではなく空文字・空配列・Null Objectを使う

null は「値がない」ことを表す便利な手段ですが、使いすぎるとnullチェックが増えます。

たとえば、コレクションは null より空配列や空リストを返したほうが扱いやすいです。

悪い例です。

C#
public List<string>? GetTags()
{
return null;
}

良い例です。

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

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

C#
foreach (var tag in GetTags())
{
Console.WriteLine(tag);
}

文字列も、意味が同じなら null より空文字を使う選択肢があります。

C#
public string Description { get; set; } = "";

ただし、「未設定」と「空文字」が違う意味を持つ場合は、string? を使うべきです。

10-3. public APIではnull許容の意図を明確にする

public APIでは、引数や戻り値のnullable注釈が利用者への契約になります。

C#
public User? FindUser(int id)

このメソッドは、見つからない場合に null を返すことが型からわかります。

C#
public User GetUser(int id)

このメソッドは、基本的に User を返すことを期待させます。見つからない場合に例外を投げるのか、必ず存在する前提なのかを明確にする必要があります。

引数も同じです。

C#
public void UpdateName(string name)

これは、name が必須であることを表します。

C#
public void UpdateNickname(string? nickname)

これは、nicknamenull を渡してもよいことを表します。

10-4. 警告を無視せず設計の見直しに使う

nullable警告は、単なるコンパイラの小言ではありません。コードの設計と実装がずれていることを教えてくれるサインです。

たとえば、次の警告が出たとします。

C#
string name = user.Nickname;

Nicknamestring? なら、ここで考えるべきことは「どう警告を消すか」ではなく、「ニックネームが未設定の場合にどう振る舞うべきか」です。

C#
string name = user.Nickname ?? user.Name;

このように、nullable警告をきっかけに仕様を明確にできます。

10-5. !演算子は最後の手段として使う

! は便利ですが、使いすぎるとnullableの価値が下がります。

悪い例です。

C#
Console.WriteLine(user!.Name);
Console.WriteLine(user.Address!.City);
Console.WriteLine(user.Email!.ToLower());

このように ! が多いコードは、null設計が曖昧な可能性があります。

改善例です。

C#
if (user is null)
{
return;
}

if (user.Address is null)
{
return;
}

Console.WriteLine(user.Name);
Console.WriteLine(user.Address.City);

どうしても ! を使う場合は、なぜnullではないと言えるのかを説明できる場所に限定しましょう。

C#
// 直前のバリデーションでNameが非nullであることを保証している
Console.WriteLine(dto.Name!.Length);

ただし、コメントで説明しなければならない ! は、設計を見直したほうがよいサインでもあります。

10-6. nullableを使った読みやすく安全なコード例

最後に、nullableを意識したコード例を見てみましょう。

C#
public class User
{
public int Id { get; }
public string Name { get; }
public string? Nickname { get; }

public User(int id, string name, string? nickname)
{
ArgumentNullException.ThrowIfNull(name);

Id = id;
Name = name;
Nickname = nickname;
}

public string GetDisplayName()
{
return Nickname ?? Name;
}
}

このクラスでは、Name は必須、Nickname は任意です。GetDisplayName では、ニックネームがあればそれを使い、なければ名前を使います。

検索処理も見てみます。

C#
public class UserService
{
private readonly List<User> _users = new();

public User? FindUser(int id)
{
return _users.FirstOrDefault(user => user.Id == id);
}

public string GetUserDisplayName(int id)
{
User? user = FindUser(id);

if (user is null)
{
return "不明なユーザー";
}

return user.GetDisplayName();
}
}

FindUser は見つからない場合に null を返すので、戻り値は User? です。呼び出し側の GetUserDisplayName ではnullチェックを行い、安全に GetDisplayName を呼び出しています。

nullableを正しく使うと、コードから設計意図が読み取れるようになります。

まとめ

csharp nullableを理解するうえで重要なのは、? の見た目だけで判断しないことです。

int? は値型に null を持たせる null許容型 であり、Nullable<int> の省略形です。一方、string? は参照型に対して「nullになる可能性がある」とコンパイラに伝える Nullable参照型 です。

null許容型では、HasValueValueGetValueOrDefault() を理解することが大切です。Nullable参照型では、stringstring? の違い、CS8602CS8618 などの警告、???.??=!is not null の使い方が重要になります。

nullableの目的は、すべての null をなくすことではありません。nullを許可する場所と許可しない場所を明確にし、実行時の NullReferenceException を減らすことです。

初心者がまず意識すべきポイントは、次の3つです。

ポイント内容
nullを許可する理由がある場合だけ ? を付ける何でもnullableにしない
警告は設計の見直しに使う! で隠す前に原因を考える
int?string? は別物値型と参照型で仕組みが違う

csharp nullableを正しく使えるようになると、コードの安全性だけでなく、読みやすさや設計の明確さも大きく向上します。nullable警告をただ消すのではなく、「この値は本当にnullになってよいのか」を考えながら使っていきましょう。