- C#
C# null許容型の使い方|??・?.演算子とis nullでnullチェック
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<int> という別の型"]
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期連続の黒字決算。