?
, ??
演算子について。
成果物
情報源
?
, ??
演算子の説明をせずにごちゃごちゃとやっているので無視することにした。
?
, ??
演算子
今回はこれがすべて。
演算子 | 意味 |
---|---|
? |
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許容参照型を有効にする
- .csprojファイルを開く
Nullable
要素をPropertyGroup
要素に追加する- 値を
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
のように?
を使う。list0
がnull
のとき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?.Member
やvalue ?? defaultValue
のような演算子を使うことになると思う。もちろんNullReferenceException
例外をcatch
してもいいし、放置して強制終了させてもいい。いずれにせよ、コンパイルエラー以外に多くの選択肢があるため、これを確定させる必要がある。
対象環境
- Raspbierry pi 3 Model B+
- Raspbian stretch 9.0 2018-11-13 ※
- bash 4.4.12(1)-release ※
- SQLite 3.29.0 ※
- C# dotnet 3.0.100
$ uname -a Linux raspberrypi 4.19.42-v7+ #1218 SMP Tue May 14 00:48:17 BST 2019 armv7l GNU/Linux