やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

C#チュートリアル(NULL許容型1)

 ?, ??演算子について。

成果物

情報源

 ?, ??演算子の説明をせずにごちゃごちゃとやっているので無視することにした。

?, ??演算子

 今回はこれがすべて。

演算子 意味
? NullReferenceExceptionを発生させず、nullを返す。
?? 左辺がnullのとき右辺を返す。左辺が非nullのとき左辺を返す。
場面 コード例
変数宣言 int? a
null返し list?.Count
デフォルト値返し list ?? new List()

null許容型・非許容型

コード例
非許容型 int a
許容型 int? a
null nullセット時
値型 非許容 コンパイルエラー
参照型 非許容 コンパイル警告

 int型などの値型でnull非許容なのにnullをセットしたら、コンパイルエラーになる。Listなどの参照型でnull非許容なのにnullをセットしたら、コンパイル警告になる。

 値型int、参照型string, Listで試したら、そういう結果になったので。(後述)

プロジェクト作成

dotnet new console -o Tutorial_Null
cd Tutorial_Null

null許容参照型を有効にする

  1. .csprojファイルを開く
  2. Nullable要素をPropertyGroup要素に追加する
  3. 値をenableに設定する
<Nullable>enable</Nullable>

コード

int型

null許容型の宣言

int? a = null;

 上記のように型の末尾に?をつけるとnull許容型として宣言できる。

もしnull非許容型にnullを代入したら

 コンパイルエラーになる。

int a = null;
error CS0037: Null 非許容の値型であるため、Null を 'int' に変換できません

 この通り、ふつうint型にnullは代入できない。先述のように?でnull許容型にしないかぎり。

もし型でなく変数名の末尾に?をつけたら

 コンパイルエラーになる。

int a? = null;
error CS1003: 構文エラーです。',' が必要です。
error CS1002: ; が必要です。
error CS1525: =' は無効です。

string型

 string型は以前までのnull非許容型でもnullを代入できる。ただし以下のような警告が出た。

string b = null;
warning CS8600: Null リテラルまたは Null の可能性がある値を Null 非許容型に変換しています。 

 エラーでなく警告になるところがint型との違い。

 以下のようにnull許容型にすればnullを代入しても警告が出ない。

string? d = null;

?演算子

  List<int> list0 = null;
//Console.WriteLine($"list.Count: {list0.Count}"); // System.NullReferenceException
  Console.WriteLine($"list.Count: {list0?.Count}");

 list0?.Countのように?を使う。list0nullのときnullを返して終了する。もし?がなければNullReferenceException例外が発生していた。

 ちなみに、list変数がnull許容型であっても同様。代入時の警告が消えるだけ。

  List<int>? list1 = null;
//Console.WriteLine($"list.Count: {list1.Count}"); // System.NullReferenceException
  Console.WriteLine($"list.Count: {list1?.Count}");

??演算子

 ??の左辺がnullのとき右辺を返す。

List<int> list0 = null ?? new List<int>() { 1, 3, 5 };

 上記のときlist0にはnew List<int>() { 1, 3, 5 }が代入される。

 左辺や右辺は変数にもできる。

List<int> list0 = null;
List<int> list1 = new List<int>() { 1, 3, 5 };
List<int> list2 = list0 ?? list1;

メンバ変数として宣言

null非許容で初期化せず

class Person
{
    public string name;
}
Console.WriteLine($"{new Person().name}");

 メンバ変数宣言public string name;のところで以下のようなコンパイル警告。

warning CS8618: Null 非許容の フィールド 'name' が初期化されていません。フィールド を null 許容として宣言することを検討してください。
warning CS0649: フィールド 'Person.name' は割り当てられません。常に既定値 null を使用します。

null許容で初期化せず

class Person
{
    public string? name;
}

 メンバ変数宣言public string? name;のところで以下のようなコンパイル警告。

warning CS0649: フィールド 'Person.name' は割り当てられません。常に既定値 null を使用します。

null非許容で初期化のときにnullセット

class Person
{
    public string name = null;
}

 メンバ変数宣言public string name = null;のところで以下のようなコンパイル警告。

warning CS8625: null リテラルを null 非許容参照型に変換できません。

null非許容でString.Empty

class Person
{
    public string name = String.Empty;
}
Console.WriteLine($"{new Person().name}");

 空行が出力される。エラーも警告もなし。

class Person
{
    public string name = String.Empty;
}
Person p = null;
Console.WriteLine($"{p.name}");
warning CS8600: Null リテラルまたは Null の可能性がある値を Null 非許容型に変換しています。
warning CS8602: null 参照の可能性があるものの逆参照です。

 逆参照って何? 調べてみてもよく分からなかった。

 というか、NullReferenceExceptionの例外が発生すると思っていたのだが……。

 文字列補完をやめたら逆参照の警告が消えた。でも例外は発生せず。

Person p = null;
Console.WriteLine(p.name);

 例外発生してくれないと?の効果が確かめられないじゃん……。

所感

 これでNULL安全なのか? よくわからない。

null安全?

 コンパイル警告だけでnull安全といえるのか? 絶対にnullをセットしたくない参照型は宣言できないの? 参照型でnull代入されたらコンパイルエラーにしたい場合もあるのでは? そこまでできてnull安全と呼ぶのでは?

null許容型

 変数宣言のとき、型名のサフィックス?があればnull許容型であると判明する。だが、?がなければnull非許容型であるかは明確でない。それが明確になるのは、dotnet3.0のC#8.0以降で、かつ.csprojファイルに<Nullable>enable</Nullable>があるという条件を満たしているときである。

null非許容型の曖昧さ

nullを代入できてしまう

 参照型で?がない非許容型にnullが代入されるとコンパイル警告が出るだけである。コンパイルエラーにはならない。だからNullReferenceExceptionが発生しうる。

 参照型のデフォルト値はnull以外ないため、nullを代入できるようにするのはやむを得ないのかもしれない。後方互換のためでもあると思う。

 そのため非許容型は「nullを代入できない型」にはならない。それを「null非許容」と表現するのが正しいか疑問。null非許容型といいつつ、nullを代入できてしまうのだから。

 許容とは一体……。

非許容という呼び名がわかりにくい

 実行結果をみると「非許容(許容しない)」とは「代入できない」という意味ではなく「代入できるが、コンパイル警告を発する」という意味になる。

 だが、言葉の意味から考えるとおかしい。「非許容」なら「代入できない」という意味であるべきでは? 変数において「許容する」とは「代入できる」ことをいうはず。現にnull許容型int? aは末尾に?をつけて許容型にすることでnullを代入できるようになった。以前までのint aではnullが代入できなかったのだから。「非許容」とはその反対で「nullが代入できない」ものだと思うはず。

 なぜ非許容という語になったかは知らない。おそらく?を末尾につけるnull許容型を新設したことによって、そうでない型と区別する必要が生じた。そこでnull許容型以外の型を、非許容型と称することにしたのだろう。

null許容しないときの挙動が複数あるせいで混乱する

 許容型の「nullを代入できる」という状態以外は「nullが代入できない」以外にもあるようだ。たとえば「nullを代入できるが、コンパイル警告を出す」「nullを代入できてコンパイル警告も出さない」。よって「できる・できない」の2元論では考えられない。そのせいで「許容」の否定形「非許容」における言葉の意味が一意に特定できず混乱を招くことになっている。

 許容以外の挙動が複数あるなら、nullへの対応パターンとそれを決定する型の宣言方法を網羅していてほしかった。それなら混乱が起きないはずだ。たとえば「null禁止型」を追加するとか。参照型においてnull代入されたらコンパイルエラーになる型である。これは許容型以上にnullへの対応を明示できる。たとえばstring! a = String.Empty;のようにnull以外の値をセットすることを強制する。そんな宣言ができたほうが良かったのでは?

 非許容型には、nullを禁止するほどの強制力はない。nullも許すしポインタ値も許す。そういう曖昧な型である。ただしコンパイル警告が出るようになる。それだけ。そしてC#8.0にはnull禁止型がないはず。だから非許容という言葉の意味が不明瞭になってしまう。

コードだけ見ても非許容型かわからない

 参照型については元からnullを代入することができた。よって、宣言時サフィックス?がないことだけをみても、非許容型かどうかは判断できない。それを判断するためには宣言の他に、dotnet3.0のC#8.0以降で、かつ.csprojファイルに<Nullable>enable</Nullable>があるかどうかも見なければならない。

 つまり、非許容型であると判別するためには以下3点が必要である。

  • バージョン
  • .csprojの設定に<Nullable>enable</Nullable>がある
  • 参照型の変数を宣言するとき、型名のサフィックス?がある

 あるいは実行結果に警告が出る。それをみて非許容型か判断すればいい。ただ、そこからどうコードを修正すればいいかは不明瞭な場合があるだろうが。

変数宣言時にnull対応を明示するなら3パターン必要?

 以下のようにできたらnullへの対応を明確にできる気がする。

null対応 値型コード例 参照型コード例
null許容 int? a string? a
null警告 int# a string# a
null禁止 int a string a

 nullを代入できるか否かは以下。

null対応 値型 参照型
null許容
null警告
null禁止

 ちなみに実際のC#8.0における非許容は以下。

null対応 値型 参照型
null許容
null非許容

 現実にはC#8.0にnull禁止型など存在しない。私の勝手な妄想。

 後方互換を考えるなら以下のほうがよいか。デフォルト記法はンパイルエラーでなく警告になる。

null対応 値型コード例 参照型コード例
null許容 int? a string? a
null警告 int a or int# a string a or string# a
null禁止 int! a string! a
  • 許容: nullを有効値のひとつとして用いる
  • 警告: nullの使い方は不問(最後の時点でnullなら警告)
  • 禁止: nullが代入されたらコンパイルエラー

 後から?を付与させずnull禁止する仕様であると明示するため!を付与することがあるかもしれない。nullでなくデフォルト値や無効値を0-1で代用する仕様とか。

デフォルト記法がnull禁止ならいいかも?

 int aと書けば、今までだと「警告なし」。C#8.0で.csprojの設定に<Nullable>enable</Nullable>があると「警告あり」を意味する変数宣言である。

 ただ、もうnull禁止をデフォルトにしてしまっていいのでは? nullを使いたいほうが特殊だと思う。

 問題は参照型はデフォルトがnullなこと。よってnullを禁止することはできない。初期化されていないときの値としてuninitializedみたいな値を内部的に持っていてくれたら判別できそうな気がするが。

null対応を宣言時に設定する

 先述の3パターンがあればnull対応を事前に定義できる気がする。変数にnullがセットされたとき、無視するか、コンパイル警告か、コンパイルエラーかを明示できる。

 ただ、これだけでNullReferenceExceptionを完全に排除することはできない。メソッドやプロパティの戻り値がnullであることもあるだろうから。そのときは各シグニチャの戻り値の型を上記のように宣言すればいい気がする。結果がnullのとき、無視・警告・エラーの対応をしてくれたらいい。

 これでnull禁止した分に関してはコンパイルエラーになってくれる。null対応については一切考える必要がない。だから是非ともnull禁止したいのだが。

 null許容・null警告に対してはnullが代入できる。よってnull時の対応を個別にコーディングする必要がある。このときInstance?.Membervalue ?? defaultValueのような演算子を使うことになると思う。もちろんNullReferenceException例外をcatchしてもいいし、放置して強制終了させてもいい。いずれにせよ、コンパイルエラー以外に多くの選択肢があるため、これを確定させる必要がある。

対象環境

$ uname -a
Linux raspberrypi 4.19.42-v7+ #1218 SMP Tue May 14 00:48:17 BST 2019 armv7l GNU/Linux