概要
null が来た時にできる対処はいくつかあります。
- null が来たら単に null を返す (対処は他の誰かに委ねる)
- null が来たら何か適当な有効な値で埋める
- null が来たら何も処理しない
- null を完全に認めない
ここでは、それぞれについて C# での書き方について説明して行きます。
null
C# の参照型には null (無効な値、何もない、ゼロ)という、無効な参照を表す特別な値があります。 また、null許容型を使うことで、 本来は無効な値を持たない値型に対しても無効な状態を表すことができます。
詳しくは外部で書いた記事「nullが生まれた背景と現在のnullの問題点」で書いていますが、null はちょっと妥協の産物で、今となっては無い方がいいとも言われます。 例えば2010年代以降に生まれたプログラミング言語であれば、
-
既定では「無効」という状態を認めない
- 必ず有効な値での初期化を求める
- 「無効」が欲しいなら、別途
Optional<T>と言うような、「無効な値、もしくは、T型の有効な値」を表す型を使う
となっているものが増えてきています。
C# では、値型 T の場合にはこれと同じような状態になっています。
- 値型では「
Tの無効な値」を表すものがない -
「無効」が欲しいなら
?を付けて、null許容型にする- 「無効」を表すのに
T?の null を使う
- 「無効」を表すのに
一方で参照型の場合、 C# 8.0 以前は意図して null (無効)を認めているのかどうかがわからないという問題がありました。 このせいで、「メソッド実装側は null を想定してないのに、呼び出し側が null を渡してしまった」などの齟齬が起こっていました。
この参照型の問題に対して C# 8.0 では null 許容参照型というものを導入しました。 null 自体はなくせないものの、少なくとも「意図して null を使っているかどうか」だけは表せるようになっています:
- 参照型でも単に型
Tと書くと null を認めない T?と書いた場合だけ null を認める
いずれにせよ、C# では、null を「無効な値」として使われます。
そして、概要でも書きましたが、null が来た時にできる対処はいくつかあります。
- null が来たら単に null を返す (対処は他の誰かに委ねる)
- null が来たら何か適当な有効な値で埋める
- null が来たら何も処理しない
- null を完全に認めない
例題
最初に、本項の残りの部分の例として使うクラスを1セット用意しておきましょう。
例えば、ゲームで使いそうなデータ構造で考えてみます。 「無効な値」というか、null を「空欄」的な意味で使うことを考えます。
- 武器の装備欄は固定で4つある
- 「1つ目と3つ目に武器を持っていて、2つ目と4つ目には何も持っていない」みたいに、歯抜けがあって、かつ、何番目かの順序も保つ
-
以下のように異なる種類の画面がある
- 空欄は飛ばして詰めて表示したい画面
- 空欄には空欄画像を出したい画面
これを、以下のように表現してみましょう。
// 武器装備欄 class WeaponSlots { // 空欄のところには null を入れる public Weapon? Weapon1 { get; } public Weapon? Weapon2 { get; } public Weapon? Weapon3 { get; } public Weapon? Weapon4 { get; } } // 武器 class Weapon { // 基礎攻撃力 public int Attack { get; } // 画像の URL public string? ImagePath { get; } // パラメーターを for 列挙できるように public int this[int parameterIndex] => parameterIndex switch { 0 => Attack, // 実際は他のパラメーター種別もあるとして… _ => throw new IndexOutOfRangeException(), }; }
この例では、何も装備していない欄を表すのに null を使うことにします。
そして、装備確認・変更画面を作ることを考えます。 これを以下ことなどを考えてみましょう。
Weaponから画像URLを得る- 画像URLを渡して、その画像ロードする
- ロードした画像を表示する
null 条件演算子(null が来たら null を返す)
Ver. 6.0
まず、Weaponから画像URL (string)を得る部分だけを見てましょう。
この時点では、null(空欄)だったらnull(無効なURL)を返すことにしましょう。
(もちろん実装によっては、この時点で「空欄だったら空欄画像を表すURLを返す」という仕様もあるかもしれませんが、ここではとりあえずこの仕様でいきます。)
この処理は、以下のように書くこともできます。
static string? M(Weapon? w) { if (w == null) return null; else return w.ImagePath; }
あるいはこれと全く同じコードを条件演算子を使って以下のように書いたりします。
static string? M(Weapon? w) { return w == null ? null : w.ImagePath; }
この類の「null が来たら null を返す」という処理はそれなりに頻出します。
そこで、もっと楽に書けるように、C# 6.0 でnull条件演算子(null conditional operator)と言うものが導入されました。
null条件演算子は、メンバー アクセスのための . の代わりに ?. を使うことで「null が来たら null を返す」という挙動をします。
すなわち、以下のコードで、先ほどと同じ挙動をします。
static string M(Weapon? w) => w?.ImagePath;
インデクサーに対するnull条件演算子
インデクサーの前にも、?を付けることでnull条件付きにできます。
static int? M(WeaponSlots w) => w.Weapon1?[0];
これは以下のようなコードとほぼ同じ意味になります。
static int? M(WeaponSlots w) { var w1 = w.Weapon1; if (w1 == null) return null; else return w1[0]; }
補足: null許容型に対するnull条件演算子
null 条件演算子 ?. を使えば、null許容型のメンバー アクセスが少し楽になります。
例えば以下のコードでは、x の行はコンパイル エラーになりますが、y の行は OK です。
// さっきと違って Weapon が構造体 struct Weapon { // 基礎攻撃力 public int Attack { get; } // 画像の URL public string ImagePath { get; } } class Program { // Weapon を構造体にしたので、null が使いたければ null 許容型にする(? を付ける) static void M(Weapon? w) { // null 許容型に対して直接 . でメンバー アクセスはできない。 // (. でアクセスできるのは Nullable<T> 構造体の HasValue や Value などのメンバーだけ) var x = w.ImagePath; // ?. なら使える。 var y = w?.ImagePath; } }
null じゃないときだけメソッド呼び出し
null 条件演算子 ?. は戻り値がない(戻り値が void の)メソッドに対しても使えます。
この場合、?. の結果も「戻り値がない」(void)扱いです。
例えば、WeaponSlots にも Weapon にも Dispose メソッドを用意したとして、
WeaponSlots は Weapon1 などが null じゃないときだけその Dispose を呼ぶとしたい場合、以下のように書けます。
public void Dispose() { Weapon1?.Dispose(); Weapon2?.Dispose(); Weapon3?.Dispose(); Weapon4?.Dispose(); }
これは以下のようなコードとほぼ同じ意味です。
public void Dispose() { if (Weapon1 != null) Weapon1.Dispose(); if (Weapon2 != null) Weapon2.Dispose(); if (Weapon3 != null) Weapon3.Dispose(); if (Weapon4 != null) Weapon4.Dispose(); }
戻り値はないので、以下のようなコードは書けません。
// void の ?. 結果は void。 // 何の値も返って来ず、変数に受けたりはできない。 var x = Weapon1?.Dispose();
補足: デリゲートの呼び出し
?[] が行けるのなら、デリゲート呼び出し時に ?() も行けそうに思えますが、
これは認められていません。
条件演算子 ? : との弁別が少し面倒で、需要の割に実装するリスクが大きいとのことで認めていないようです。
ただ、デリゲートは d() のような呼び方の他に、d.Invoke() と言う呼び方もできるので、
こちらなら null 条件演算子 ?. が使えます。
using System.ComponentModel; class Bindable : INotifyPropertyChanged { public event PropertyChangedEventHandler? PropertyChanged; protected void OnPropertyChanged(PropertyChangedEventArgs args) => PropertyChanged?.Invoke(this, args); }
null合体演算子(null が来たら何か適当な有効な値で埋める)
Ver. 2.0
次に、画像URLを渡して、その画像ロードする部分を考えましょう。
この段階で、「空欄(ImagePathとしてもnullが渡ってくる)の時には空欄画像を読む」という処理を入れてみます。
例えば以下のように書けるでしょう。
(ここではLoadImage(string path)という名前で画像を読み込むメソッドがあるものして説明します。)
const string EmptyWeaponSlotImagePath = "EmptyWeaponSlot.png"; static Image LoadWeaponImage(string? imagePath) { string path; if (imagePath == null) path = EmptyWeaponSlotImagePath; else path = imagePath; return LoadImage(path); } static Image LoadImage(string path) { // 画像読み込み処理(省略、ここでは仮に new Image() を返す) return new Image(); }
前節と同様、この「null の時に所定の値に差し替える」と言う処理も頻出です。
こちらは C# 2.0で、null合体演算子(null coalescing operator)と言うものが導入されました。
以下のように、??で、左側に元の値、右側に差し替えたい値を書きます。
static Image LoadWeaponImage(string? imagePath) { return LoadImage(imagePath ?? EmptyWeaponSlotImagePath); }
ちなみに、別のページでも書いていますが、coalesce を「合体」と訳すのはちょっとわかりにくいかもしれません。 coalesce には「(折れた骨が)融合・癒着する」と言うような意味があって、 例えば欠けた素材をパテなどで穴埋めするようなときにも使うようです。 「null coalescing」 も null で欠けた部分を穴埋めすると言うようなニュアンスです。
補足: null条件演算子とnull合体演算子の短絡評価
null条件演算子とnull合体演算子はいわゆる短絡評価になっています。 null条件演算子の場合は左側がnullだったら、 null合体演算子の場合は左側がnullでなかったら、右側を評価する必要がなくなるので、全く評価しません。
例えば、プロパティやメソッドがどこまで呼ばれたのかを確認するためのログ表示を仕込んだ以下のようなクラスを用意します。
static class Extension { // null な変数に対しても a.M(i) で例外を起こさず呼べる拡張メソッド。 public static void M(this A? s, int i) { Console.WriteLine("A.M(int)"); } public static int M(this int i) { Console.WriteLine("int.M()"); return i; } } class A { public A? X { get { // プロパティが読まれたことを確認するためだけのログ表示。 Console.WriteLine("X"); return field; } set; } }
これに対して3通りの呼び出し方をしてみましょう。
まず、非 null しかない場合、?. から先がすべて呼ばれます。
// 変数も、その X も非 null の場合 var a1 = new A { X = new() }; // X も呼ばれ、M も呼ばれる。 // X, int.M(), A.M(int) の3行表示される。 a1?.X?.M(1.M());
X int.M() A.M(int)
続いて、変数は非 null、その X は null の場合、
X?. の後ろが呼ばれなくなります。
この時、引数の評価(この例の場合、1.M() の部分)も消えます。
// 変数は非 null、その X は null の場合 var a2 = new A { X = null }; // a1 は 非 null → X は呼ばれる // その X は null → M は呼ばれない // M を呼ばなくていいならその引数の 1.M() 自体呼ばれない // X の1行だけ表示される。 a2?.X?.M(1.M());
X
最後に、根本がすでに null の場合、すべて呼ばれなくなります。
// 変数自体が null の場合 A? a3 = null; // a3 が null の時点で X もその先も呼ばれない // 何も表示されない。 a3?.X?.M(1.M());
余談: キャッシュ用途
null を使う場面の例としてよく挙げられるものの1つに、キャッシュ用途もあります。 ここでいうキャッシュは、
-
クラスのコンストラクターの時点では計算できない、もしくは、計算したくない
- 「計算自体にそこそこコストが掛かるので、計算は1回限りにしたい」など
- 1度計算してしまえばその後値は変化しない
- 未計算の状態として null を使って、「null の時だけ計算」というような処理を書く
というものです。
例えば以下のように書いたりします。 リフレクションを使った例ですが、リフレクションは重たいので取得した値はキャッシュしておきたいです。
using System; using System.ComponentModel; using System.Reflection; // System.Type から、自分のプログラムで使う属性とかを抽出するためのクラス class TypeInfo { private readonly Type _type; public TypeInfo(Type type) => _type = type; // 必ずしも使わないものとする。使うときにだけ属性を読みたい。 // リフレクションは重たいので、1回呼んだらキャッシュしておきたい。 public string Description { get { if (_description == null) { var desc = _type.GetCustomAttribute<DescriptionAttribute>(); _description = desc?.Description ?? ""; } return _description; } } private string? _description; }
こういう場合、以下のように、 ?? を使ってもっと短縮して書くこともできます。
1行だけにできるので、=> を使えたりもします。
public string Description => _description = _description ?? _type.GetCustomAttribute<DescriptionAttribute>()?.Description ?? "";
ただ、この例はちょっと1行に詰め込みすぎではあるので、??から後ろは別途メソッド化する方が読みやすくていいでしょう。
public string Description => _description = _description ?? GetDescription(); private string GetDescription() => _type.GetCustomAttribute<DescriptionAttribute>()?.Description ?? "";
Ver. 8.0
C# 8.0 で入った??= 演算子は、こういうキャッシュ用途で使うのに特に便利です。
上記の例は以下のように書くことができます。
public string Description => _description ??= GetDescription();
Ver. 14
ちなみに、この手のコードに対しては C# 14 で導入された field キーワードが有効で、 C# 14 以降では以下のような書き方ができます。
// (_description フィールドを用意する必要なし。) public string Description => field ??= GetDescription();
nullを読み飛ばす
続いて、ロードした画像の表示を考えます。 今回の例では画像を表示する画面には2種類あって、「空欄は飛ばして詰めて表示したい画面」という仕様のものもあります。
単純に null が来たら飛ばすだけでいいので、要は、以下のような if を書きます。
void ShowImage(Weapon? w) { var imageUrl = w?.ImagePath; if (imageUrl != null) { Canvas.Draw(LoadImage(imageUrl)); } }
Ver. 7.0
また、C# 7.0で導入されたパターン マッチングは、この手の null 判定のためにも使えます。 例えば先ほどのコードは以下のように書くこともできます。
void ShowImage(Weapon? w) { if (w?.ImagePath is string imageUrl) { Canvas.Draw(LoadImage(imageUrl)); } }
ちなみに、is var (varパターンと言って、is T とは別扱い)を使った場合、nullチェックはされません。
var は何でも受け取れる構文で、null も受け付けます。
Ver. 8.0
C# 8.0 では、再帰パターン の {} が暗黙的に null チェックも含んでいることを使って、手短に null チェックができます
(参考: 非 null マッチング)。
string? s = null; if (s is var _) Console.WriteLine("ここは通る"); if (s is { }) Console.WriteLine("ここは通らない");
null を完全に認めない
今回の例では、画面に2種類の仕様がありますが、
- 前節の「空欄は飛ばして詰める」というものでは、
ifステートメントで null を読み飛ばしているので、Drawの行には絶対にnullが来ない - 「空欄画像を表示する」という仕様でも、
LoadWeaponImageの時点で有効な空欄画像を読んでいるはずなので、Drawの行には絶対にnullが来ない
と言うように、画像の表示メソッドCanvas.Drawに対しては絶対にnullが渡らないはずです。
ここにnullが来てしまうということは、何らかのバグがあるということです。
しっかりとテストをして、そういうことが起こらないようにデバッグすべきものです。
最初に説明した通り、本来、「型をTとだけ書けばnullを絶対に受け付けない。nullを受け付けたければT?と書く」とすべきです。
C# 8.0 でこの仕様が入る予定ですが、現時点(C# 7.3)では残念ながら、これができるのは値型だけです。
C# 7.3 以前の場合、せめて、引数に対してnull判定をして、nullだったら例外を出すということをよくやります。
class Canvas { public void Draw(Image image) { if (image == null) { throw new ArgumentNullException(nameof(image)); } // 描画処理 } }
Ver. 7.0
ちなみに、C# 7.0ではthrow式といって、??の右側にthrowを書けるようになったので、以下のような書き方でnull判定を行うこともできます。
class Canvas { public void Draw(Image image) { image = image ?? throw new ArgumentNullException(nameof(image)); // 描画処理 } }
余談: 自称 null
混乱の元なのでおすすめはしませんが、演算子を自作して、「null を自称できる型」を作ることができます。 例えば以下のようなものです。
// null じゃないのに this == null が成り立ってしまうかなりタチが悪いクラス class FalseNullable { // 動作確認用 public string? Name { get; } // 自身が null でなくても、中身が null だったら null を自称する public bool IsNull => Name == null; // 自称 null public static readonly FalseNullable Null = new FalseNullable(); public FalseNullable() => Name = null; public FalseNullable(string name) => Name = name; // IsNull が true のとき、null とも一致 public static bool operator ==(FalseNullable? x, FalseNullable? y) => ReferenceEquals(x, y) || (y is null && x.IsNull) || (x is null && y.IsNull); public static bool operator !=(FalseNullable? x, FalseNullable? y) => !(x == y); // 自称 null のときは "null" と表示 public override string? ToString() => IsNull ? "null" : Name; }
タチが悪いことに、この型には「真の null」(null)と「自称 null」(FalseNullable.Null)があります。
真のnullと自称nullで、is演算子や??演算子の挙動が変わります。
例えば、上記のクラスに対して以下のような処理を書いたとします。
static void Write(FalseNullable? x) { Console.WriteLine(x); // == 演算子呼び出し。自称 null でも true になる。 Console.WriteLine(x == null); // これは == を呼ばない。真の null の時だけ true になる。 Console.WriteLine(x is null); // == 呼ばない。自称 null の時には "null" と出る。 Console.WriteLine(x ?? new FalseNullable("coalescing value")); // わざと null 参照。これも、例外になるのは真の null の時だけ try { Console.WriteLine(x.Name); } catch { Console.WriteLine("NullReferenceException"); } }
== では、ユーザー定義の==演算子が呼ばれて、自称nullがx == nullを満たします。
一方、isでは、真のnullしかx is nullになりません。
また、?? で右辺の値が選ばれるのも真のnullの時だけです。
x.Nameがnull参照例外になるのも真のnullの時だけになります。
例えば以下のような呼び出しをすると、
Console.WriteLine("=== 真の null ==="); Write(null); Console.WriteLine("=== 自称 null ==="); Write(FalseNullable.Null); Console.WriteLine("=== 非 null ==="); Write(new FalseNullable("non-null"));
以下のような結果になります。
=== 真の null === True True coalscing value NullReferenceException === 自称 null === null True False null === 非 null === non-null False False non-null non-null
かなり気持ち悪い挙動を起こしますので、「null を自称できる型」を作るのはよっぽどのことがない限り辞めた方がいいでしょう。
