C# の null許容型とは、型名の後ろに ? をつけて「この変数には null が入ることがある」と示す書き方です。 int? のような値型につける場合と、string? のような参照型につける場合では仕組みが異なり、ここを区別できると null まわりのコードがすっきり読めるようになります。

この記事では、int? と string? の違いを最初に図で分け、?.・?[]・??・??=・!・is null・is not null を対応バージョンつきの一覧表にまとめます。さらに、#nullable enable で出る警告 CS8600・CS8602・CS8618 を実際に再現して直し方をパターン別に示し、== null と is null の違い、HasValue・GetValueOrDefault、C# 14 の null条件代入まで扱います。コードは .NET SDK 8.0.131(C# 12)で実行し、出力をそのまま載せています。C# 14 の機能は動かせなかったため、その箇所には「未実行」と明記しました。

null許容型と null 関連の演算子の早見表

書き方 名前 意味 使えるバージョン
int? a null許容値型 値型に null を入れられるようにする C# 2.0
string? s null許容参照型 参照型に「null があり得る」と注釈する C# 8.0
a ?? b null合体演算子 a が null なら b、そうでなければ a C# 2.0
a ??= b null合体代入演算子 a が null のときだけ b を代入する C# 8.0
a?.B null条件演算子(メンバー) a が null なら null、そうでなければ a.B C# 6.0
a?[i] null条件演算子(要素) a が null なら null、そうでなければ a[i] C# 6.0
a! null免除演算子 「ここは null ではない」とコンパイラに伝えて警告を消す C# 8.0
a is null 定数パターン a が null かを判定する C# 7.0
a is not null 否定パターン a が null でないかを判定する C# 9.0
a is { } x プロパティパターン null でなければ x に取り出す C# 8.0
a?.B = 値 null条件代入 a が null でないときだけ代入する C# 14
a.HasValue / a.Value Nullable のプロパティ 値があるか/値を取り出す(値型のみ) C# 2.0
a.GetValueOrDefault(既定値) Nullable のメソッド 値がなければ既定値を返す(値型のみ) C# 2.0
やりたいこと 書き方
null なら既定値を使う var name = input ?? "未設定";
null でなければメンバーを呼ぶ var len = text?.Length;
null でも 0 として扱う var len = text?.Length ?? 0;
null のときだけ初期化する list ??= new List<string>();
null なら例外にする ArgumentNullException.ThrowIfNull(arg);(.NET 6 以降)
null かどうかで分岐する if (x is null) { } / if (x is not null) { }

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「C# null許容型の使い方|??・?.演算子とis nullでnullチェック」

int? と string? の違いを図で分ける

同じ ? でも、値型につけた場合と参照型につけた場合では、まったく別の機能 です。

flowchart TD
    A["型名の後ろに ? をつける"] --> B{"もとの型はどちら?"}
    B -->|"値型: int / bool / DateTime / struct / enum"| C["null許容値型  int?"]
    B -->|"参照型: string / 配列 / class"| D["null許容参照型  string?"]
    C --> E["実体は Nullable&lt;int&gt; という別の型"]
    E --> F["実行時にも区別がある<br>HasValue / Value が使える"]
    D --> G["型は string のまま<br>コンパイラへの注釈"]
    G --> H["実行時の区別はない<br>警告 CS86xx で知らせる"]
比べる点 int?(null許容値型) string?(null許容参照型)
導入されたバージョン C# 2.0 C# 8.0
? なしで null を入れると コンパイルエラー 警告(#nullable enable のとき)
実体 Nullable<int> という構造体 string そのもの
実行時に型の違いがあるか ある ない
HasValue / Value 使える 使えない
有効にする設定 不要(常に使える) #nullable enable またはプロジェクトファイルの <Nullable>enable</Nullable>
目的 値型で「値がない」を表す null になり得る場所をコンパイル時に見つける

実行して確かめます。

#nullable enable
int? b = 5;
Console.WriteLine($"typeof(int?)={typeof(int?)}");
Console.WriteLine($"GetUnderlyingType={Nullable.GetUnderlyingType(typeof(int?))}");
Console.WriteLine($"b.GetType()={b.GetType()}");
Console.WriteLine($"sizeof(int)={sizeof(int)}, int? のサイズ={System.Runtime.CompilerServices.Unsafe.SizeOf<int?>()}");
Console.WriteLine($"typeof(List<string?>) == typeof(List<string>): {typeof(List<string?>) == typeof(List<string>)}");
typeof(int?)=System.Nullable`1[System.Int32]
GetUnderlyingType=System.Int32
b.GetType()=System.Int32
sizeof(int)=4, int? のサイズ=8
typeof(List<string?>) == typeof(List<string>): True
  • int? は Nullable<int> という別の型 で、「値があるかどうか」の情報を持つぶんサイズも大きくなっています(int の 4 バイトに対して 8 バイト)。
  • List<string?> と List<string> は実行時には同じ型 です。string? の ? はコンパイラが警告を出すための注釈で、実行時の動きは変えません。
  • b.GetType() が System.Int32 になるのは、GetType を呼ぶときに中の値が取り出されるためです。

int? など null許容値型の使い方

HasValue・Value・GetValueOrDefault

int? a = null;
int? b = 5;

Console.WriteLine($"a.HasValue={a.HasValue}, b.HasValue={b.HasValue}, b.Value={b.Value}");

try { Console.WriteLine(a.Value); }
catch (InvalidOperationException e) { Console.WriteLine($"{e.GetType().Name}: {e.Message}"); }

Console.WriteLine($"a.GetValueOrDefault()={a.GetValueOrDefault()}");
Console.WriteLine($"a.GetValueOrDefault(-1)={a.GetValueOrDefault(-1)}");
Console.WriteLine($"b.GetValueOrDefault(-1)={b.GetValueOrDefault(-1)}");
Console.WriteLine($"a ?? -1 = {a ?? -1}");
Console.WriteLine($"[{a}] [{b}]");
a.HasValue=False, b.HasValue=True, b.Value=5
InvalidOperationException: Nullable object must have a value.
a.GetValueOrDefault()=0
a.GetValueOrDefault(-1)=-1
b.GetValueOrDefault(-1)=5
a ?? -1 = -1
[] [5]
書き方 null のとき 値があるとき 使いどころ
a.HasValue false true 値があるかの判定(a is not null と同じ意味)
a.Value InvalidOperationException その値 値があると確認できた後
a.GetValueOrDefault() その型の既定値(int なら 0) その値 null を 0 として扱いたい
a.GetValueOrDefault(-1) 指定した値 その値 既定値を指定したい
a ?? -1 右側の値 その値 既定値を指定したい(短く書ける)
文字列補間 {a} 空文字 値の文字列 表示用

null のまま .Value を読むと InvalidOperationException になります。値を取り出すときは ?? か GetValueOrDefault を使うと、null の場合の値も一緒に決められます。

パターンマッチングで取り出す

判定と取り出しを同時に書けるのがパターンマッチングです。

int? a = null;
int? b = 5;
if (b is int v) Console.WriteLine($"b is int v -> {v}");
if (a is not int) Console.WriteLine("a is not int");
if (b is { } nn) Console.WriteLine($"b is {{ }} -> {nn}");
if (b is > 3) Console.WriteLine("b is > 3");
b is int v -> 5
a is not int
b is { } -> 5
b is > 3

b is int v は「b に値があれば、int 型の変数 v に取り出す」という意味です。HasValue を確認してから Value を読む2段階が、1行で書けます。

null を含む計算と比較

null許容値型どうしを計算すると、どちらかが null なら結果も null になります。比較は少し動きが異なります。

int? a = null;
int? b = 5;
int? sum = a + b;
Console.WriteLine($"a + b is null: {sum is null}");
Console.WriteLine($"a > b: {a > b}, a < b: {a < b}, a == b: {a == b}, a == null: {a == null}, a != b: {a != b}");

bool? t = true, u = null, f = false;
Console.WriteLine($"true & null = [{t & u}], false & null = [{f & u}], true | null = [{t | u}], false | null = [{f | u}]");
a + b is null: True
a > b: False, a < b: False, a == b: False, a == null: True, a != b: True
true & null = [], false & null = [False], true | null = [True], false | null = []
式 結果 補足
null + 5 null 算術は null が伝わる
null > 5 / null < 5 どちらも false 大小比較は、片方が null なら常に false
null == null true 等価比較は null どうしなら true
true & null null 結果が決まらないので null
false & null false 片方が false なら結果は false に決まる
true | null true 片方が true なら結果は true に決まる

a > b が false だからといって a <= b が true とは限らない点が、つまずきやすいところです。null があり得る値を比較するときは、先に ?? で既定値に置き換えるか、is null で分けておく と結果が読みやすくなります。

文字列から数値への変換で使う

「変換できなければ null」を返す形は、null許容値型のよくある使い道です。

int? parsed = int.TryParse("12x", out var pv) ? pv : null;
Console.WriteLine($"parsed is null: {parsed is null}");

var nums = new int?[] { 3, null, 5 };
Console.WriteLine($"Sum={nums.Sum()}, Max={nums.Max()}, Average={nums.Average()}, Count(not null)={nums.Count(x => x is not null)}");
parsed is null: True
Sum=8, Max=5, Average=4, Count(not null)=2

LINQ の Sum・Max・Average は、null の要素を除いて計算 します。Average が 4(8 ÷ 2)になっているのがその証拠です。

string? など null許容参照型の使い方

有効にする方法

null許容参照型は、次のどちらかで有効にします。

<!-- プロジェクトファイル(.csproj)で全体を有効にする -->
<PropertyGroup>
  <Nullable>enable</Nullable>
</PropertyGroup>
// ファイル単位で有効にする(ファイルの先頭に書く)
#nullable enable

dotnet new console で作ったプロジェクトファイルには、最初から <Nullable>enable</Nullable> が入っていました(.NET SDK 8.0.131 で確認)。有効にすると、string は「null を入れない型」、string? は「null があり得る型」 として扱われ、コンパイラが代入や呼び出しを確認してくれます。

無効な状態(#nullable disable)で string? と書くと、次の警告が出ました。

warning CS8632: The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

実行時には止めてくれない点に注意

null許容参照型はコンパイル時の確認なので、警告を無視したり ! で消したりすると、実行時には NullReferenceException が起こり得ます。

#nullable enable
string? maybe = null;
try { Console.WriteLine(maybe!.Length); }
catch (NullReferenceException e) { Console.WriteLine($"{e.GetType().Name}: {e.Message}"); }
NullReferenceException: Object reference not set to an instance of an object.

!(null免除演算子)は「警告を消す」だけで、null を別の値に変える機能はありません。使うのは、コンパイラには判断できないが、自分では null でないと確実にわかっている場所 に絞ります。

null条件演算子でnullのときだけ飛ばす

?. と ?[] は、左側が null ならそこで評価をやめて null を返し、null でなければ通常どおりアクセス します。

#nullable enable
string? name = null;
Console.WriteLine($"name?.Length is null: {name?.Length is null}");
Console.WriteLine($"name?.Length ?? 0 = {name?.Length ?? 0}");
Console.WriteLine($"name?.ToUpper() ?? \"(なし)\" = {name?.ToUpper() ?? "(なし)"}");

int[]? arr = null;
Console.WriteLine($"arr?[0] is null: {arr?[0] is null}");
arr = new[] { 10, 20 };
Console.WriteLine($"arr?[0] = {arr?[0]}");

Dictionary<string, string>? dict = null;
Console.WriteLine($"dict?[\"k\"] ?? \"default\" = {dict?["k"] ?? "default"}");
name?.Length is null: True
name?.Length ?? 0 = 0
name?.ToUpper() ?? "(なし)" = (なし)
arr?[0] is null: True
arr?[0] = 10
dict?["k"] ?? "default" = default

ポイントは戻り値の型です。name.Length は int ですが、name?.Length は null になり得るので int? になります。int として使いたいときは ?? 0 を続けます。

何段もたどるとき

#nullable enable
Order? order = new Order();
Console.WriteLine(order?.Customer?.Address?.City ?? "未登録");

class Address { public string? City { get; set; } }
class Customer { public string Name { get; set; } = ""; public Address? Address { get; set; } }
class Order { public Customer? Customer { get; set; } public int Add(int x) => x; }
未登録

Customer が null なので、その先の Address や City は評価されず、全体が null になり、?? の右側が使われます。if 文で1段ずつ確認する書き方と比べて、短く書けます。

右側は評価されない(短絡評価)

左側が null のとき、その後ろのメソッド呼び出しや引数の式は実行されません。

#nullable enable
Order? none = null;
int calls = 0;
int Next() { calls++; return 1; }

var r = none?.Add(Next());
Console.WriteLine($"calls={calls}");
calls=0

Next() は一度も呼ばれていません。引数に副作用のある処理(カウンタの更新やログ出力など)を書いている場合は、この動きを覚えておくと挙動を読み違えずに済みます。

イベントやデリゲートの呼び出し

#nullable enable
Action? handler = null;
handler?.Invoke();
Console.WriteLine("handler?.Invoke() ok");
handler?.Invoke() ok

デリゲートが null でも例外にならず、登録されているときだけ呼び出されます。

bool? になるときの条件式

?. の結果が bool のメソッドだと、戻り値は bool? になり、そのまま if の条件には書けません。== true をつける のが定番です。

#nullable enable
string? name = null;
Console.WriteLine($"name?.StartsWith(\"a\") == true: {name?.StartsWith("a") == true}");
Console.WriteLine($"name?.Length > 0: {name?.Length > 0}");
name?.StartsWith("a") == true: False
name?.Length > 0: False

name?.Length > 0 は「null と 0 の大小比較」になり、前述のとおり false になります。null のときは条件を満たさない、という意図ならこの書き方で足ります。

null合体演算子でnullのときの値を決める

a ?? b は、a が null でなければ a、null なら b を返します。??= は、左側が null のときだけ代入します。

#nullable enable
string? s1 = null, s2 = null, s3 = "c";
Console.WriteLine($"s1 ?? s2 ?? s3 = {s1 ?? s2 ?? s3}");

List<string>? tags = null;
tags ??= new List<string>();   // null なので代入される
tags.Add("a");
tags ??= new List<string>();   // null ではないので何もしない
Console.WriteLine($"tags.Count={tags.Count}");

int? cnt = null;
cnt ??= 10;
cnt ??= 20;
Console.WriteLine($"cnt={cnt}");

Console.WriteLine($"[{"" ?? "x"}] / [{(string.IsNullOrEmpty("") ? "x" : "")}]");
s1 ?? s2 ?? s3 = c
tags.Count=1
cnt=10
[] / [x]
  • ?? は続けて書けます。左から順に見て、最初に null でなかった値 が使われます。
  • ??= の2回目は、すでに値が入っているので何も起きません。
  • ?? が見るのは null だけ です。空文字 "" は null ではないので、そのまま空文字が返ります。空文字も既定値に置き換えたいときは string.IsNullOrEmpty や string.IsNullOrWhiteSpace で判定します。

null なら例外にする

引数が null のときに処理を止めたい場合は、?? と throw を組み合わせるか、.NET 6 以降の ArgumentNullException.ThrowIfNull を使います。

#nullable enable
string? name = null;
try { string x = name ?? throw new ArgumentNullException(nameof(name)); }
catch (ArgumentNullException e) { Console.WriteLine($"{e.GetType().Name}: {e.Message}"); }

try { ArgumentNullException.ThrowIfNull(name); }
catch (ArgumentNullException e) { Console.WriteLine($"{e.GetType().Name}: {e.Message}"); }
ArgumentNullException: Value cannot be null. (Parameter 'name')
ArgumentNullException: Value cannot be null. (Parameter 'name')

ThrowIfNull は引数名を自動で拾ってくれるので、nameof を書かずに同じメッセージになります。

== null と is null の違い

null の判定には == null と is null の2つの書き方があります。違いは、クラスが == 演算子をオーバーロード(独自に定義)しているときに現れます。 あえて「何と比べても等しい」と答える == を持つクラスで試します。

#nullable enable
var w = new Weird();
Console.WriteLine($"w == null: {w == null}");
Console.WriteLine($"w is null: {w is null}");
Console.WriteLine($"w is not null: {w is not null}");
Console.WriteLine($"ReferenceEquals(w, null): {ReferenceEquals(w, null)}");
Console.WriteLine($"(object)w == null: {(object)w == null}");

class Weird
{
    public static bool operator ==(Weird? x, Weird? y) => true;
    public static bool operator !=(Weird? x, Weird? y) => false;
    public override bool Equals(object? o) => true;
    public override int GetHashCode() => 0;
}
w == null: True
w is null: False
w is not null: True
ReferenceEquals(w, null): False
(object)w == null: False

w には実際にインスタンスが入っているのに、w == null は True と答えています。クラスが定義した == が呼ばれたためです。一方、is null は演算子のオーバーロードを呼ばず、参照が null かどうかだけを判定 するので、正しく False になります。

書き方 オーバーロードされた == 使えるバージョン おすすめ度
x == null / x != null 呼ばれる C# 1.0 既存コードに合わせるときに使う
x is null 呼ばれない C# 7.0 null 判定の基本として使う
x is not null 呼ばれない C# 9.0 null でない判定の基本として使う
x is { } 呼ばれない C# 8.0 判定と同時に変数へ取り出したいとき
ReferenceEquals(x, null) 呼ばれない C# 1.0 == を定義するクラスの内部など

新しく書くコードでは is null / is not null を使う と、クラスの作りに左右されず、意図も英語の文のように読めます。なお、int? に対してはどちらの書き方でも結果は同じです。

#nullable enable で出る警告を再現して直す

null許容参照型を有効にすると、null になり得る場所でコンパイラが警告を出します。代表的なものを実際に出してみます。

#nullable enable
string? input = Console.ReadLine();
string s = input;                 // 3行目
Console.WriteLine(input.Length);  // 4行目
string t = null;                  // 5行目
Show(input);
var m = new Member();
m.Name = null;                    // 8行目
int? n = null;
int k = n.Value;                  // 10行目
static void Show(string x) => Console.WriteLine(x);
static string Get() { string? z = null; return z; }  // 12行目
class Member
{
    public string Name { get; set; }   // 15行目
    public string Email;               // 16行目
}

dotnet build の出力から、null に関する警告を抜き出したものです(.NET SDK 8.0.131)。

Program.cs(3,12): warning CS8600: Converting null literal or possible null value to non-nullable type.
Program.cs(4,19): warning CS8602: Dereference of a possibly null reference.
Program.cs(5,12): warning CS8600: Converting null literal or possible null value to non-nullable type.
Program.cs(8,10): warning CS8625: Cannot convert null literal to non-nullable reference type.
Program.cs(10,9): warning CS8629: Nullable value type may be null.
Program.cs(12,48): warning CS8603: Possible null reference return.
Program.cs(15,19): warning CS8618: Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
Program.cs(16,19): warning CS8618: Non-nullable field 'Email' must contain a non-null value when exiting constructor. Consider declaring the field as nullable.

6行目の Show(input) には警告が出ませんでした。4行目で input.Length を読んだ後は、コンパイラが「ここまで来たなら null ではない」と判断するためです。

警告 意味 出る場面
CS8600 null かもしれない値を、null を入れない型に代入している string s = input;(input は string?)
CS8602 null かもしれないものの中身にアクセスしている input.Length
CS8603 null かもしれない値を、null を返さない型のメソッドから返している return z;
CS8618 null を入れない型のプロパティ・フィールドが、コンストラクター終了時に未設定 public string Name { get; set; }
CS8625 null リテラルを、null を入れない型に代入している m.Name = null;
CS8629 null かもしれない null許容値型の値を読んでいる n.Value

CS8600 の直し方

パターン 直す前 直した後
既定値を決める string s = input; string s = input ?? "";
null もあり得ると認める string s = input; string? s = input;
null なら処理を止める string s = input; string s = input ?? throw new ArgumentNullException(nameof(input));

CS8602 の直し方

パターン 直す前 直した後
先に判定する input.Length if (input is not null) { input.Length }
null なら飛ばす input.Length input?.Length ?? 0
空文字もまとめて除く input.Length if (!string.IsNullOrEmpty(input)) { input.Length }
自作の判定メソッドを使う if (IsFilled(input)) input.Length 引数に [NotNullWhen(true)] をつける

is not null や string.IsNullOrEmpty で判定した後のブロックでは、コンパイラが「null ではない」と理解するので警告が消えます。自作の判定メソッドでも同じ効果を得たいときは、System.Diagnostics.CodeAnalysis の NotNullWhen 属性を使います。

CS8618 の直し方

パターン 書き方 向いている場面
初期値を入れる public string Name { get; set; } = ""; 空文字で始めてよい
null を認める public string? Nick { get; set; } 値がないことに意味がある
コンストラクターで受け取る public Member2(string name) => Name = name; 必ず値を渡してほしい
required をつける(C# 11 以降) public required string Name { get; set; } オブジェクト初期化子で必ず指定してほしい
後から必ず設定されるとわかっている public string Late { get; set; } = null!; フレームワークが後から値を入れる場合など。使う場所を絞る

直した後のコード

上の直し方を当てはめた全体です。ビルドして警告が 0 件になることを確認しました。

#nullable enable
using System.Diagnostics.CodeAnalysis;

string? input = Console.ReadLine();
string s = input ?? "";
if (input is not null) Console.WriteLine(input.Length);
Console.WriteLine(input?.Length ?? 0);
if (!string.IsNullOrEmpty(input)) Console.WriteLine(input.Length);
if (IsFilled(input)) Console.WriteLine(input.Length);

var m = new Member { Name = "a" };
var m2 = new Member2("a");
var m3 = new Member3();
Console.WriteLine(m.Name + m2.Name + m3.Name + s);

static bool IsFilled([NotNullWhen(true)] string? x) => x is not null && x.Length > 0;

class Member { public required string Name { get; set; } }
class Member2 { public string Name { get; set; } public Member2(string name) => Name = name; }
class Member3
{
    public string Name { get; set; } = "";
    public string? Nick { get; set; }
    public string Late { get; set; } = null!;
}
record Member4(string Name, string? Nick = null);
    0 Warning(s)

null を含む配列から null を除く

string? の配列から null を除いて List<string> にしたいとき、Where だけでは型が string? のまま残ります。次の2つの書き方で、警告なしに List<string> を得られました。

#nullable enable
var strs = new string?[] { "a", null, "b" };
List<string> clean = strs.Where(x => x is not null).Select(x => x!).ToList();
List<string> clean2 = strs.OfType<string>().ToList();
Console.WriteLine($"{clean.Count} {clean2.Count}");
2 2

OfType<string>() は null を除きつつ型も string に変えてくれる ので、! を書かずに済みます。

C# 14 の null条件代入

C# 14(.NET 10 に付属)では、?. と ?[] を代入の左側にも書ける ようになりました。今回の環境は C# 12 のため、この節のコードは未実行で、内容は Microsoft 公式ドキュメント「What's new in C# 14」で確認したものです。

// C# 13 まで
if (customer is not null)
{
    customer.Order = GetCurrentOrder();
}

// C# 14 以降(未実行)
customer?.Order = GetCurrentOrder();
項目 内容(公式ドキュメントで確認)
書ける場所 ?. と ?[] を、代入・複合代入の左側に書ける
右側の評価 左側が null でないときだけ評価される(customer が null なら GetCurrentOrder は呼ばれない)
使える演算子 = と、+=・-= などの複合代入
使えない演算子 インクリメント ++ とデクリメント --

C# 12 で同じ書き方をビルドすると、次のコンパイルエラーになりました(実行結果)。

#nullable enable
O? p = null;
p?.X = 1;
p?.X += 1;
class O { public int X; }
Program.cs(4,1): error CS0131: The left-hand side of an assignment must be a variable, property or indexer
Program.cs(5,1): error CS0131: The left-hand side of an assignment must be a variable, property or indexer

このエラーが出たら、使っている C# のバージョンが 13 以前 だとわかります。その場合は、if 文で is not null を確認してから代入する書き方にします。

つまずきやすい点と直し方

起きること 原因 こうするとよい
Nullable object must have a value の例外 null の int? で .Value を読んだ ?? か GetValueOrDefault で取り出す
NullReferenceException ! で警告を消したが実際は null だった ! をやめて is not null で判定する
?. の結果を int に代入できない name?.Length の型は int? ?? 0 を続ける
if の条件に bool? を書けない x?.IsOk の型は bool? x?.IsOk == true と書く
?? で空文字が置き換わらない ?? が見るのは null だけ string.IsNullOrEmpty で判定する
a > b も a <= b も false になる 片方が null の大小比較は常に false 先に ?? で既定値にする
== null の結果が合わない クラスが == を独自に定義している is null を使う
CS8618 が大量に出る 既存コードで null許容参照型を有効にした ファイル単位で #nullable enable を入れて少しずつ対応する
CS0131(代入の左側にできない) C# 13 以前で a?.B = 値 と書いた if 文で判定してから代入する

null チェックの書き方を選ぶ判断表

状況 選ぶ書き方
null なら既定値でよい ??
null ならその処理を飛ばしてよい ?. / ?[]
null のときだけ初期化したい ??=
null は想定外なので止めたい ArgumentNullException.ThrowIfNull
null かどうかで処理を分けたい is null / is not null
判定と同時に値を取り出したい is int v / is { } x
コンパイラには伝わらないが null でないと確実にわかる !(場所を絞って使う)

よくある質問

Q. int? と Nullable<int> は違うものですか? A. 同じものです。int? は Nullable<int> の短い書き方で、typeof(int?) を表示すると System.Nullable の型になります。HasValue・Value・GetValueOrDefault は、この Nullable 構造体のメンバーです。

Q. string? と書いても実行時に null を防いでくれないのはなぜですか? A. null許容参照型はコンパイル時の注釈で、実行時の型は string のままだからです。警告を解消していくことで null の見落としを減らす仕組みなので、外部から受け取る値(入力やファイルの内容など)は、受け取った場所で is null や ThrowIfNull を使って確認します。

Q. ?? と ?. はどう使い分けますか? A. ?. は「null ならその先へ進まない」、?? は「null なら代わりの値を使う」です。text?.Length ?? 0 のように組み合わせると、「null でなければ長さ、null なら 0」を1行で書けます。

Q. is null と == null はどちらを使えばよいですか? A. is null をおすすめします。クラスが == 演算子を独自に定義していても影響を受けず、参照が null かどうかだけを判定するためです。C# 9.0 以降なら、否定も is not null と書けます。

まとめ

  • int? は Nullable 構造体という別の型、string? はコンパイラへの注釈 で、同じ ? でも仕組みが異なる
  • 値型は HasValue・Value・GetValueOrDefault が使える。null のまま Value を読むと InvalidOperationException になる
  • ?. と ?[] は null ならそこで止まり、?? は null のときの値を決め、??= は null のときだけ代入 する
  • is null / is not null は演算子のオーバーロードを呼ばない ので、null 判定の基本として使える
  • CS8600 は代入、CS8602 はアクセス、CS8618 は未初期化のプロパティ への警告。既定値・判定・required などで直せる
  • ! は警告を消すだけ で実行時の null は防がない。使う場所を絞る
  • C# 14 では a?.B = 値 と代入の左側にも書ける(.NET 10。C# 13 以前では CS0131)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「C# null許容型の使い方|??・?.演算子とis nullでnullチェック」

株式会社RJCでは、C#のnull安全なコードの書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?