やってみる

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

C#チュートリアル(Null許容型2)

 新しい構文がたくさん出てきてチュートリアルが進められない……。

情報源

 例によって新しい構文の説明もなしに、知っている前提で話している感じ。私はそれらにつてい全く知らない。よって、まずは未知の構文を洗い出すことにした。ユースケースを理解するのはそれから。ドキュメントもそうして欲しい。理解すべき順序からしてそうだろJK。

 コード例をGitHubから入手することになっているが、サイズが大きすぎるのでやめる。最小限のコードを自分で書いてみる。リポジトリを個別に分けてほしい。できれば一部ディレクトリのみ入手できるようになってほしい。

未知の構文

<LangVersion>8.0</LangVersion>

  • C#のバージョンを指定する
  • csprojファイルに追加することで

 バージョンを指定する意味がわからない。おそらくnull安全に関する構文への対処を決定する要素のひとつなのだろう。警告メッセージなどがバージョンごとに変わるようになったときなどを想定しているのかも?

 <Nullable>enable</Nullable>も追加することで、はじめてnull許容型が使えるようになる、と思う。このとき変数宣言の末尾に?がないものはnull非許容型となる。C#8.0において非許容型にnullが代入されたら、値型ならコンパイルエラー(これまで通り)、参照型ならコンパイル警告となる。

#nullable

 ソースコードのうち、#nullableディレクティブ以降のコードは指定したnullableコンテキストになる。nullへの対応が決まる。たとえば#nullable enableと書けば、以降の行でnull許容型を宣言できるようになる。

 <Nullable>enable</Nullable>にすると、そのプロジェクト全体がnull注釈コンテキストになる。既存プロジェクトの場合、それだと全箇所での修正が必要となり大変になってしまう。そこで一部だけnull許容型を使いたいときなどの場合、その箇所を#nullableディレクティブで囲う。

default

 defaultは各型に応じた既定値を返す。型ごとの既定値は規定値の一覧を参照。

int i = default(int);
string s = default(string);

 C#7.1以降は以下のようにdefaultリテラルが使える。

int i = default;
string s = default;

 defaultリテラルは他にも以下のような箇所で使える。

public void method1(int a = default) {}
public int method2() { return default; }
public void method3(int a) {}
method3(default);

* !演算子

 要点は以下。

  • null許容注釈コンテキスト内で使う
  • 変数の末尾に!を付与することで使える
  • その値(式)がnullでないことを宣言する
  • 目的は警告CS8625を回避すること(意図したnull使用時)
public string Uri { get; set; } = default!;

 使う場面は「意図したnull使用時」である。たとえば以下2つ。

!使用例1: 単体テスト

 以下コードはnullのとき例外発生することを期待する。まず#nullableでnull許容コンテキストにする。このときstringなどの参照型は?がないかぎりnull非許容型となる。以下コードは非許容型である。もしnull非許容型にnullを代入するとコンパイル時に警告が出る。

実装コード

#nullable enable
public class Person
{
    public Person(string name) => Name = name ?? throw new ArgumentNullException(nameof(name));
    public string Name { get; }
}

 上記コードのうち例外発生をテストしたい。このときテストコード側でnullリテラルを使う。先述の通り#nullable enableのnull非許容型に対してnullを渡すため警告が出る(Warning CS8625)。ただ、実装コード側はそれでいいが、テストコード側は警告されたくない。意図してnullを使うため警告は不要である。このとき、null!のように末尾に!をつけることで警告を回避できる。

テストコード

[TestMethod, ExpectedException(typeof(ArgumentNullException))]
public void NullNameShouldThrowTest()
{
    var person = new Person(null!);
}

!使用例2: 論理的に非null確定(コンパイラにはわからない)

public static void Main()
{
    Person? p = Find("John");
    if (IsValid(p))
    {
        Console.WriteLine($"Found {p!.Name}");
    }
}

public static bool IsValid(Person? person)
{
    return person != null && !string.IsNullOrEmpty(person.Name);
}

 式の結果がnullにならないことが明確だが、コンパイラがそれを認識できないときもある。このときコンパイラは警告を出す。この警告を回避したいとき!が使える。

 上記コードはnullにならないことが明確なため、警告は無用の長物である。そこで警告を回避するため!を付与している。p!がそれである。

 pの値はPerson? pのように宣言されており、null許容型である。型としてはnullを代入できるが、if文とその条件IsValidメソッドによりnullにはなり得ない。ただ、コンパイラはそれを理解できない。コンパイラにわかることは、Person?のようにnull許容型として宣言されたことだけだ。つまりnullになりうることだけわかっている。よって、$"{p.Name}"とすると警告が出でしまう。

 コンパイラがnullにならないことを理解できていないせいで起こる。回避したいとき!を付与して$"{p!.Name}"とする必要がある。

最初から?をつけずnull非許容にすればいいのでは?

 最初から?をつけずnull非許容にすればいいのでは? と思う。だが、null許容かつ!にすべき場合もある。たとえば途中まではnullが入りうる場合があり、最終的には非nullであるとき。そのときは今回のようなnull許容型かつ!を使うことになる。

 null非許容型にできなかった理由がある。今回のコード例でいえばFind()メソッドの仕様のせいだろう。Find("John")は指定した名前Johnが見つかればJohnPersonインスタンスを返す。だが見つからなければnullを返す仕様だろう。つまり、Find("John")メソッド戻り値の型はnull許容型Person?のはずだ。それを受け取る側の変数型も同じ型である必要がある。よってnullを返す仕様であることから、null許容型にせざるを得ない。

 もしFind()メソッド内でPersonを見つけられなかったとき、例外を発生させる仕様なら、null非許容型でよかったし、!も不要だったろう。もっとも、そのときはtry〜cahtch構文で握りつぶすか、プログラム中断することになるだろうが。

 例外発生はパフォーマンス悪化に繋がるし、対応コードも長くなる。よって、#nullable enable, ?, !, nullを使ったコーディングのほうが良いと判断しうる。nullを完全排除できない理由はいくつかあると思うが、パフォーマンスを気にする限りこれからもnullと付き合っていかねばならないと思われる。

型におけるnull値の許容パターン

null 概要
非許容 null割当不可
許容 null割当可。nullチェックなしに逆参照すると警告。
無関係 C#8.0以前の状態。警告なし。
不明 指示なし状態。

 ここには以下のように書いてあったが本当か?

  • "null 非許容" : この型の変数には、null を割り当てることはできません。 この型の変数については、逆参照する前に null チェックを行う必要はありません。

 つまり、以下は不可だと? 警告が出るだけでは?

#nullable enable
string s = null;

nullコンテキスト

コンテキスト設定

設定方法コンテキストnull許容
<Nullable>注釈警告参照型
enable
warnings
annotations
disable
設定方法コンテキスト
#nullable注釈警告
enable
disable
restorepjpj
enable warnings
disable warnings
restore warningspj
enable annotations
disable annotations
restore annotationspj

 公式によるとrestoreは「プロジェクト設定に戻す」らしいが、ここでは「1つ前の設定に戻す」とある。どっちだよ。

 以下、設定例。

.csproj

<Nullable>enable<Nullable>

.cs

#nullable enable
...
#nullable disable

所感

 理解するだけで大変そう。以下のようなことを把握したいのだが、そんな資料があるのかわからん。

  1. null安全の概念
  2. C#におけるnullとnull安全の考え方
  3. C#8.0における実装済みのnull安全対策
  4. C#null安全対策への展望

 せめてnull安全に関するC#8.0構文だけをまとめた文書とかないの?

対象環境

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