null許容型関連QA
「null許容」という名前は同じでも、中身は全くの別物ではないか?
はい、その通りです。名前に「null許容」と付いているため混同しやすいですが、「null許容値型」と「null許容参照型」は全くの別物です。
null許容値型(例: int?)
実体は Nullable<T> という構造体(値型)です。コンパイル時だけでなく、実行時にも型そのものが変わります。この構造体は、「値を持っているか」を表す bool 型のフラグ(HasValue)と、実際の値(Value)を保持しています。
null許容参照型(例: string?)
こちらはコンパイラの静的解析のための仕組み(アノテーション)に過ぎません。実行時の型は通常の参照型(string)と全く同じであり、CLR(共通言語ランタイム)から見れば両者に違いはありません。
名前は似ていますが、「実行時の実体が変わるnull許容値型」と「コンパイラのチェック用マーカーであるnull許容参照型」という明確な違いを意識しておくことが重要です。
null許容参照型を使えば、実行時のNullReferenceExceptionは完全に防げるのか?
残念ながら、完全には防げません。
前述の通り、null許容参照型はあくまで「コンパイラによる警告」という形での支援です。警告を無視してコードをコンパイル・実行することは可能ですし、実行時の振る舞いは旧来の参照型と全く同じです。
また、リフレクションを使った動的な値の割り当てや、null許容参照型が無効な古いライブラリとの境界(相互運用)、あるいはnull免除演算子(!)の乱用などによって、実行時に null が混入する経路はいくつも存在します。あくまで「静的解析によってバグに気づきやすくするツール」として捉えるのが正解です。
null免除演算子(!)を使えば、null許容値型を元の型に変換できるのではないか?
いいえ、できません。
!(null免除演算子 / null-forgiving operator)は、コンパイラに対して「ここは絶対にnullじゃないから警告を出さないでくれ」と伝えるためのものであり、型を変換(キャスト)する能力はありません。
例えば、以下のようなコードを書くとコンパイルエラーになります。
int? nullableInt = 10; // コンパイルエラー! 型 'int?' を 'int' に暗黙的に変換できません。 int normalInt = nullableInt!;
! を付けても変数の型は int? のままです。非nullの値型(int)に代入するには、明示的なキャスト (int)nullableInt を行うか、.Value プロパティにアクセスする必要があります。! は主に参照型の警告抑制に使われるものだと覚えておきましょう。
null許容値型をボックス化すると、Nullable<T>という構造体がボックス化されるのか?
いいえ、特殊な挙動をします。
通常、構造体を object 型などに代入すると、その構造体そのものがヒープ上にボックス化されます。しかし、Nullable<T> をボックス化する場合、CLRは特別扱いをします。
値がある(HasValue が true)場合
Nullable<T> 構造体ではなく、中身の T 型の値そのものがボックス化されます。(例:int? なら純粋な int としてボックス化される)
値がない(HasValue が false / null)場合
ボックス化されたオブジェクトは生成されず、単なる null 参照になります。
この仕様のおかげで、int? を object に入れても、取り出すときに int として自然に扱うことができ、直感的なプログラミングが可能になっています。
x.Valueと(int)xによる値の取り出しに違いはあるのか?
実行時の挙動には、違いはありません。
int? x = null; の状態から値を取り出そうとした場合、x.Value を使っても (int)x とキャストしても、どちらも内部的には同じ処理が呼ばれ、結果として InvalidOperationException がスローされます。
プロジェクト全体でnull許容参照型を有効にしないと、?アノテーションは使えないのか?
いいえ、ファイル単位や行単位で局所的に有効化することが可能です。
既存の巨大なプロジェクトを最新のC#に移行する際、プロジェクト全体(.csproj)で <Nullable>enable</Nullable> を設定すると、膨大な数の警告が出てビルドログが埋め尽くされてしまうことがあります。
そういった場合は、ソースコードの先頭や特定の範囲に#nullableディレクティブを記述することで、段階的に導入できます。
#nullable enable: これ以降の行でnull許容参照型を有効にする#nullable disable: これ以降の行で無効にする#nullable restore: プロジェクトのデフォルト設定に戻す
それぞれの後ろにwarningsを付けて#nullable disable warningsとすると、null参照の可能性があるときの警告を抑制します。#nullable enable warningsと#nullable restore warningsというディレクティブにより警告を有効にしたりプロジェクト設定に戻したりできます。
値型の配列と、null許容値型の配列ではメモリ配置が大きく変わるのか?
いいえ、どちらも全ての要素が隙間なく並んだ連続したメモリ領域が確保されます。
しかし、int? の実体はあくまで Nullable<int> という構造体(値型)です。したがって、int?[] を作成した場合、配列のメモリ領域には「値の有無を管理する bool フラグ」と「実際の int の値」をセットにした構造体が、隙間なく連続して配置されます。
ただし、bool型のフラグが加わるため、データサイズが変わります。例えばint型なら4バイトですが、int?型の場合にはbool型の1バイトが加わって5バイトとなり、さらにメモリアラインメントにより4バイト境界に配置されるため、各要素は合計で8バイトのサイズになります。
なお、パックされた構造体の場合には、一つの要素のサイズが5バイトになりますし、各要素の先頭アドレスも1バイト単位になって全要素がびっしり詰めて配置されます。