nullについて
null許容参照型 について整理したかったのですが、C# 7.3 以前のプログラムとの共存問題をどのように手を付けて良いか悩んでしまったので、まずはnullについて雑記としてつらつらと書き記す次第です。
(記事内容は特に整理されたものではありません)
null
参照型の変数において、インスタンスへの参照が設定されていない状態。
参照型と値型
- 値型 :
int
やbool
等の組み込み値型(単純型とも呼ばれる) や 構造体、列挙型 が値型に分類される。
値型の変数には値そのものが格納され、C#においては値が格納されていない状態が存在しない(値型にはnull
という状態が無い)。1
変数に値そのものが格納されているので、変数へ代入を行う度に値が複製される。 - 参照型 :
string
やobject
の他、クラスやインターフェイスで定義されたいわゆるオブジェクトが参照型に分類される。
参照型の変数にはオブジェクトの実体(インスタンス)そのものが格納されるわけでは無く、メモリ上に展開されたインスタンスのデータへの参照2が格納される。
この参照が格納されていない状態をnull
と呼んでいる。
変数にはオブジェクトそのものは格納されていないので、オブジェクトが代入の都度複製(クローン)されることは無い(変数に格納された参照のみ複製して格納される)。
nullの説明について
null
の説明として「何も無い状態」といった記述を見かけるが、これを「データそのものが存在しない状態」として誤解されるケースが見受けられた(特に新入社員等の若手)。
C#の変数に着目した場合、null
と言うのは「データへの参照が無い状態」で、言い換えれば「データへアクセスできない状態」と言った方が良いと思う(ちなみに、Wikipediaでは「何も示さないもの」と紹介されている)。
例えば、以下のようなコードの場合、
var info = new FileInfo(@"略"); // 1
info = null; // 2
- 1 の時点で、
FileInfo
クラス のインスタンスが新しく作成され、メモリ上にそのデータが展開される。
info
という変数にはそのインスタンスへの参照が格納される。 - 2 の時点で、
info
変数に設定されていたインスタンスへの参照が破棄される(変数はどのインスタンスへの参照も格納されていない状態)。
この時点では、info
変数 を通して 1 で作成されたインスタンスにアクセスすることは出来なくなっているが、インスタンスそのものは確かに存在しているので、厳密に「何も無い状態」になったとは言えない。3
NullReferenceException
参照型の変数を通してインスタンスメンバー(メソッド、プロパティ等)を利用することになる。
変数.メンバー
と記述した場合、変数に格納された参照先に存在するインスタンスのメンバーにアクセスすることになるが、変数が null
の場合はメンバーを持つインスタンスに対してアクセスが出来ない状態となるので、例外がスローされる。
この時にスローされる例外が NullReferenceException
である。4
NullReferenceException
をスローさせない為には、変数の状態が null
であるか事前に判断する必要があり、これを nullチェック と呼んでいる。
var hoge = /* 略 */; // nullかも知れない参照型変数
if ( hoge == null )
{
// 変数がnullの場合はメソッドの実行を行わない
}
else
{
hoge.Exec(); // nullで無い場合のみメソッドを実行する
}
null条件演算子 と null合体演算子
参照型の変数が null
の場合に処理の振り分けを行う機能がある。
if文 でも同様の処理が記述できるが、null
か否か に着目するケースではより簡潔に書けるようになる場合がある。
null条件演算子
?.
で表される演算子。変数が null
であれば、null
を返す。
-
変数?.メンバー
-
変数
がnull
で無い 場合、演算子は変数.メンバー
の結果を返す -
変数
がnull
の場合、演算子はnull
を返す
-
以下の if
文 と同様の処理となる。
if ( 変数 != null )
{
return 変数.メンバー;
}
else
{
return null;
}
null合体演算子
??
で表される演算子。変数が null
であれば、指定した値 を返す。
-
対象 ?? 指定値
-
対象
がnull
で無い 場合、演算子は対象
を返す -
対象
がnull
の場合、演算子は指定値
を返す
-
if ( 対象 != null )
{
return 対象;
}
else
{
return 指定値;
}
上記を組み合わせる
以下のように組み合わせて使用することも多い。5
-
変数?.メンバー ?? 指定値
-
変数
がnull
で無い 場合、変数.メンバー
を実行する-
変数.メンバー
の結果がnull
で無い場合、変数.メンバー
の結果を返す -
変数.メンバー
の結果がnull
の場合、指定値
を返す
-
-
変数
がnull
の場合、指定値
を返す
-
null許容値型
通常、値型は null
と言う状態にはならず、フィールド等で明示的な値を代入しなかった場合に初期値(int
型の場合 0
)が設定されてしまう。
この仕様により、意図的に初期値と同じ値を代入した場合 と 何ら値を代入していない場合 が区別できず困る場合がある。
あるいは データベース 等の値が格納されていない状態を NULL
として区別するアプリケーションを利用する場合に「値型だけどnullと言う状態(値が存在しない状態)」を表現したい場合がある。
これらに null許容値型(Nullable
型)が利用できる。
null許容値型は 値型 T
に対して T?
と記述する(例えば、int
型 なら int?
)。
null許容値型 は Nullable<T>
構造体 と等価で、T?
型 と Nullable<T>
型 は同じ型の変数となる。
また、構造体(値型) なので null許容値型 の変数そのものが null
になることは無い。
Nullable<T>
構造体 は以下のプロパティを持ち、これを利用して「nullと言う状態を持つ値型」が定義できる。
-
HasValue
: null許容値型 の変数に値が格納されている場合true
、そうで無い場合(null
に相当)はfalse
を返す。 -
Value
: null許容値型 の変数に格納された値を返す。
ただし、値が格納されていない場合(HasValue
プロパティがfalse
の場合)はInvalidOperationException
例外がスローされる。
null許容参照型
C# 8.0 で導入された新しい概念。
参照型は null
という状態が存在し、通常の値型は null非許容値型 と言うならば 参照型は null許容参照型 と言えるので、普通のことを言っているように思える。
この概念は「参照型でも安易に null
を許容しない方が良い」と言う考え方に基づき、参照型の変数でも「null
を許容しない」ことを基本としていく考え方と認識している。
と言うのも、プログラムを書く上で null
の扱いは非常に難しく、先述の nullチェック を適切に行わなければ意図せず例外がスローされることになり、つまりバグを生む原因となりうるからである。
言い換えれば「null
であること」を適切に管理しなければバグにつながるので、基本的には参照型の変数であっても null
を許容しない方が都合が良いとも考えられる(逆に言えば、null
であることを意図する場合においてのみ null
を許容する)。
null許容参照型 を導入すると、以下のように 参照型 の扱いが変わる。
- 参照型
T
の変数がnull
の状態になるコードが警告対象となる。- 変数に
null
を代入した場合(またはnull
となる可能性があるメソッド等の結果を代入した場合) - フィールドやプロパティを初期化しない場合
- 変数に
- 参照型
T
に対してT?
と記述することで、null許容参照型 を宣言できる。- null許容参照型の変数に
null
を代入しても警告とはならない(従来の参照型と同様)。
- null許容参照型の変数に
-
null
の可能性がある参照型変数に対して null チェックせずにそのメンバーにアクセスするコードが警告対象となる。- 適切な nullチェック を行うことで警告とならなくなる。
string str; // 参照型の変数
str = null; // null を代入すると警告となる
class Hoge
{
public string Fuga { get; set; }
public Hoge()
{
// Fuga を初期化しない場合、警告となる
}
}
string? str; // null許容参照型の変数
str = null; // null を代入しても警告とならない
string? str = /* 略 */; // nullの可能性があるメソッド等の戻り値を格納
int i1 = str.Length; // nullに対してメンバーのアクセスを行うと警告
int i2 = default;
if ( str != null )
{
i2 = str.Length; // nullチェックを行えば警告とならない
}
意図せず null を代入してしまうことや、nullチェック の漏れが警告によって検出されるので非常に便利な機能だが、これを単純に導入してしまうと C# 7.3 以前のコードが警告だらけになる。6
既存のアプリケーションを有効活用しつつ C# 8.0 以降でコーディングする為に null許容参照型 とどのように付き合っていくか悩み中(本当はここをまとめたかった)。
その他、細かいこと
レコード型
C# 9.0 で導入された レコード型(record
) は参照型となる。
C# 10.0 で レコード構造体型(record struct
) が導入され、これは値型となる。
必須メンバー
C# 11.0 で導入された 必須メンバー(required
) は、オブジェクト初期化子で初期値を指定することを強制する仕組み。
初期値が指定されない場合はエラーとなるが、null
を指定してもエラーは解消される。
(そのメンバーが null
を許容しない参照型であれば、警告は表示される)
まとめ(では無い)
null許容参照型 との自分なりの付き合い方が見つかれば、追記修正するか新たに記事を書き起こすつもりです。
-
int
型 であれば0
、bool
型 であればfalse
といったように、明示的に値を格納しない場合の初期値が定められている。 ↩ -
メモリ上のアドレスのような情報。言語によってポインタとも呼ばれる(厳密にはポインタとは異なる概念のようですが)。 ↩
-
このインスタンスについて、どの変数からも参照されていない(インスタンスへの参照が格納された変数が存在しない)状態になった場合、ガベージコレクションによってメモリ上からデータが抹消される。
この時点で「何も無い状態」になるが、null
とは別の概念と考えた方が良い。 ↩ -
Java では同様の例外が
NullPointerException
にあたり、これを略して「ぬるぽ」と呼ぶことがある。C# なら「ぬるり」とでもなりそうだが、大抵「ぬるぽ」と呼ばれている気がする。 ↩ -
変数
がnull
の場合と変数.メンバー
の結果がnull
の場合 のいずれも指定値
を返すことに注意。 ↩ -
C# 7.3 以前では 参照型
T
の変数はnull
を代入することが可能で警告も出なかったが、C# 8.0 以降で単にT
型 として変数の宣言をするとnull
に対しての警告が表示されるようになった為。
なお、このように既存のコード体系と考え方が変わってしまう為、明示的に null許容参照型 の機能を有効化した場合に限り、警告が出るように設計されている(有効化しなければ C# 7.3 以前のコードが警告だらけになることはない)。 ↩