[雑記] nullについて(C#)

# nullについて

:::note warn
null許容参照型 について整理したかったのですが、C# 7.3 以前のプログラムとの共存問題をどのように手を付けて良いか悩んでしまったので、まずはnullについて雑記としてつらつらと書き記す次第です。
(記事内容は特に整理されたものではありません)
:::

## null

参照型の変数において、インスタンスへの参照が設定されていない状態。

### 参照型と値型

* 値型 : `int` や `bool` 等の組み込み値型(単純型とも呼ばれる) や 構造体、列挙型 が値型に分類される。
  値型の変数には値そのものが格納され、C#においては値が格納されていない状態が存在しない(値型には `null` という状態が無い)。[^1]
  変数に値そのものが格納されているので、変数へ代入を行う度に値が複製される。
* 参照型 : `string` や `object` の他、クラスやインターフェイスで定義されたいわゆるオブジェクトが参照型に分類される。
  参照型の変数にはオブジェクトの実体(インスタンス)そのものが格納されるわけでは無く、メモリ上に展開されたインスタンスのデータへの参照[^2]が格納される。
  この参照が格納されていない状態を `null` と呼んでいる。
  変数にはオブジェクトそのものは格納されていないので、オブジェクトが代入の都度複製(クローン)されることは無い(変数に格納された参照のみ複製して格納される)。

[^1]: `int`型 であれば `0`、`bool`型 であれば `false` といったように、明示的に値を格納しない場合の初期値が定められている。
[^2]: メモリ上のアドレスのような情報。言語によってポインタとも呼ばれる(厳密にはポインタとは異なる概念のようですが)。

### nullの説明について

`null` の説明として「何も無い状態」といった記述を見かけるが、これを「データそのものが存在しない状態」として誤解されるケースが見受けられた(特に新入社員等の若手)。
C#の変数に着目した場合、`null` と言うのは「データへの参照が無い状態」で、言い換えれば「データへアクセスできない状態」と言った方が良いと思う(ちなみに、[Wikipedia](https://ja.wikipedia.org/wiki/Null "Null - Wikipedia")では「何も示さないもの」と紹介されている)。

例えば、以下のようなコードの場合、

```csharp
var info = new FileInfo(@"略"); // 1
info = null; // 2
```

* 1 の時点で、`FileInfo`クラス のインスタンスが新しく作成され、メモリ上にそのデータが展開される。
  `info` という変数にはそのインスタンスへの参照が格納される。
* 2 の時点で、`info`変数に設定されていたインスタンスへの参照が破棄される(変数はどのインスタンスへの参照も格納されていない状態)。
  この時点では、`info`変数 を通して 1 で作成されたインスタンスにアクセスすることは出来なくなっているが、インスタンスそのものは確かに存在しているので、厳密に「何も無い状態」になったとは言えない。[^3]

[^3]: このインスタンスについて、どの変数からも参照されていない(インスタンスへの参照が格納された変数が存在しない)状態になった場合、ガベージコレクションによってメモリ上からデータが抹消される。
この時点で「何も無い状態」になるが、`null` とは別の概念と考えた方が良い。

## NullReferenceException

参照型の変数を通してインスタンスメンバー(メソッド、プロパティ等)を利用することになる。
`変数.メンバー` と記述した場合、変数に格納された参照先に存在するインスタンスのメンバーにアクセスすることになるが、変数が `null` の場合はメンバーを持つインスタンスに対してアクセスが出来ない状態となるので、例外がスローされる。
この時にスローされる例外が `NullReferenceException` である。[^4]

[^4]: Java では同様の例外が `NullPointerException` にあたり、これを略して「ぬるぽ」と呼ぶことがある。C# なら「ぬるり」とでもなりそうだが、大抵「ぬるぽ」と呼ばれている気がする。

`NullReferenceException` をスローさせない為には、変数の状態が `null` であるか事前に判断する必要があり、これを nullチェック と呼んでいる。

```csharp
var hoge = /* 略 */; // nullかも知れない参照型変数

if ( hoge == null )
{
    // 変数がnullの場合はメソッドの実行を行わない
}
else
{
    hoge.Exec(); // nullで無い場合のみメソッドを実行する
}
```

## null条件演算子 と null合体演算子

参照型の変数が `null` の場合に処理の振り分けを行う機能がある。
if文 でも同様の処理が記述できるが、`null`か否か に着目するケースではより簡潔に書けるようになる場合がある。

### null条件演算子

`?.` で表される演算子。変数が `null` であれば、`null` を返す。

* `変数?.メンバー`
  * `変数` が `null` で無い 場合、演算子は `変数.メンバー` の結果を返す
  * `変数` が `null` の場合、演算子は `null` を返す

以下の `if`文 と同様の処理となる。

```csharp
if ( 変数 != null )
{
    return 変数.メンバー;
}
else
{
    return null;
}
```

### null合体演算子

`??` で表される演算子。変数が `null` であれば、指定した値 を返す。

* `対象 ?? 指定値`
  * `対象` が `null` で無い 場合、演算子は `対象` を返す
  * `対象` が `null` の場合、演算子は `指定値` を返す

```csharp
if ( 対象 != null )
{
    return 対象;
}
else
{
    return 指定値;
}
```

### 上記を組み合わせる

以下のように組み合わせて使用することも多い。[^5]

* `変数?.メンバー ?? 指定値`
  * `変数` が `null` で無い 場合、`変数.メンバー` を実行する
    * `変数.メンバー` の結果が `null` で無い場合、`変数.メンバー` の結果を返す
    * `変数.メンバー` の結果が `null` の場合、`指定値` を返す
  * `変数` が `null` の場合、`指定値` を返す

[^5]: `変数` が `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チェック を行うことで警告とならなくなる。

```csharp
string str; // 参照型の変数
str = null; // null を代入すると警告となる
```

```csharp
class Hoge
{
    public string Fuga { get; set; }
    public Hoge()
    {
        // Fuga を初期化しない場合、警告となる
    }
}
```

```csharp
string? str; // null許容参照型の変数
str = null;  // null を代入しても警告とならない
```

```csharp
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許容参照型 とどのように付き合っていくか悩み中(本当はここをまとめたかった)。

[^6]: C# 7.3 以前では 参照型 `T` の変数は `null` を代入することが可能で警告も出なかったが、C# 8.0 以降で単に `T`型 として変数の宣言をすると `null` に対しての警告が表示されるようになった為。
なお、このように既存のコード体系と考え方が変わってしまう為、明示的に null許容参照型 の機能を有効化した場合に限り、警告が出るように設計されている(有効化しなければ C# 7.3 以前のコードが警告だらけになることはない)。

## その他、細かいこと

### レコード型

C# 9.0 で導入された レコード型(`record`) は参照型となる。
C# 10.0 で レコード構造体型(`record struct`) が導入され、これは値型となる。

### 必須メンバー

C# 11.0 で導入された 必須メンバー(`required`) は、オブジェクト初期化子で初期値を指定することを強制する仕組み。
初期値が指定されない場合はエラーとなるが、`null` を指定してもエラーは解消される。
(そのメンバーが `null` を許容しない参照型であれば、警告は表示される)

## まとめ(では無い)

null許容参照型 との自分なりの付き合い方が見つかれば、追記修正するか新たに記事を書き起こすつもりです。