概要
null が来た時にできる対処はいくつかあります。
- null が来たら単に null を返す (対処は他の誰かに委ねる)
- null が来たら何か適当な有効な値で埋める
- null が来たら何も処理しない
- null を完全に認めない
ここでは、それぞれについて C# での書き方について説明して行きます。
null
C# の参照型には null (無効な値、何もない、ゼロ)という、無効な参照を表す特別な値があります。 また、null許容型を使うことで、 本来は無効な値を持たない値型に対しても無効な状態を表すことができます。
詳しくは外部で書いた記事「nullが生まれた背景と現在のnullの問題点」で書いていますが、null はちょっと妥協の産物で、今となっては無い方がいいとも言われます。 例えば2010年代以降に生まれたプログラミング言語であれば、
-
既定では「無効」という状態を認めない
- 必ず有効な値での初期化を求める
- 「無効」が欲しいなら、別途
Optional<T>と言うような、「無効な値、もしくは、T型の有効な値」を表す型を使う
となっているものが増えてきています。
C# では、値型の場合にはこれと同じような状態になっています。
- 既定では無効な値がない
-
「無効」が欲しいなら
?を付けて、null許容型にする- null許容型に対しても、「無効」を表すのに null を使う
一方で、参照型の場合、 C# 8.0 以前は意図して 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; } }
この例では、何も装備していない欄を表すのに null を使うことにします。
そして、装備確認・変更画面を作ることを考えます。 これを以下の3段に分けて考えましょう。
Weaponから画像URLを得る- 画像URLを渡して、その画像ロードする
- ロードした画像を表示する
null 条件演算子(null が来たら null を返す)
Ver. 6.0
まず、Weaponから画像URL (string)を得る部分だけを見てましょう。
この時点では、null(空欄)だったらnull(無効なURL)を返すことにしましょう。
(もちろん実装によっては、この時点で「空欄だったら空欄画像を表すURLを返す」という仕様もあるかもしれませんが、ここではとりあえずこの仕様でいきます。)
この処理は、以下のように書くこともできます。
static string M(Weapon w) { string path; if (w == null) path = null; else path = w.ImagePath; return path; }
しかし、この類の「null が来たら null を返す」という処理はそれなりに頻出します。
そこで、もっと楽に書けるように、C# 6.0 でnull条件演算子(null conditional operator)と言うものが導入されました。
null条件演算子は、メンバー アクセスのための . の代わりに ?. を使うことで「null が来たら null を返す」という挙動をします。
すなわち、以下のコードで、先ほどと同じ挙動をします。
static string M(Weapon w) => w?.ImagePath;
インデクサーに対するnull条件演算子
インデクサーの前にも、?を付けることでnull条件付きにできます。
// s の i 文字目を取得 // ただし、s が null の時は null を返す static char? Write(string s, int i) => s?[i];
補足: 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; } }
補足: デリゲートの呼び出し
?[] が行けるのなら、デリゲート呼び出し時に ?() も行けそうに思えますが、
これは認められていません。
条件演算子 ? : との弁別が少し面倒で、需要の割に実装するリスクが大きいとのことで認めていないようです。
ただ、デリゲートは 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); }
前節と同様、この「null の時に所定の値に差し替える」と言う処理も頻出です。
こちらは C# 2.0で、null合体演算子(null coalescing operator)と言うものが導入されました。
以下のように、??で、左側に元の値、右側に差し替えたい値を書きます。
static Image LoadWeaponImage(string imagePath) => LoadImage(imagePath ?? EmptyWeaponSlotImagePath);
ちなみに、別のページでも書いていますが、coalesce を「合体」と訳すのはちょっとわかりにくいかもしれません。 coalesce には「(折れた骨が)融合・癒着する」と言うような意味があります。(nullで)欠けた部分を穴埋めすると言うようなニュアンスです。
補足: null条件演算子とnull合体演算子の短絡評価
null条件演算子とnull合体演算子はいわゆる短絡評価になっています。 null条件演算子の場合は左側がnullだったら、 null合体演算子の場合は左側がnullでなかったら、右側を評価する必要がなくなるので、全く評価しません。
using System; class A { public B B { get; set; } } class B { public C C { get; set; } } class C { public void M() => Console.WriteLine("呼ばれた"); } class Program { // 全部に ?. // a, B, C、null の時点でそこから後ろは呼ばれない static void M1(A a) => a?.B?.C?.M(); // a の直後にだけ ?. // a が null なら、そこから後ろは呼ばれない static void M2(A a) => a?.B.C.M(); static void Main() { M1(new A { B = new B { C = new C() } }); // 呼ばれた M1(new A { B = new B() }); // 何も表示されない M1(new A()); // 何も表示されない M1(null); // 何も表示されない M2(new A { B = new B { C = new C() } }); // 呼ばれた M2(null); // 何も表示されない M2(new A { B = new B() }); // 実行時エラーになる M2(new A()); // 実行時エラーになる } }
using System; class Program { static string GetString() { Console.WriteLine("呼ばれた"); return ""; } static string M(string s) => s ?? GetString(); static void Main() { M("abc"); // 何も表示されない M(null); // 呼ばれた } }
余談: キャッシュ用途
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();
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(LoadWeaponImage(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("coalscing 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 を自称できる型」を作るのはよっぽどのことがない限り辞めた方がいいでしょう。
