C#クイズとその解説

元ネタはEric Lippertのブログです。コードは少し改変していますが、言っていることは同じです。

- [A dynamic definite assignment puzzle](https://ericlippert.com/2018/11/14/a-dynamic-definite-assignment-puzzle/)
- [A dynamic definite assignment puzzle, part 2](https://ericlippert.com/2018/11/19/a-dynamic-definite-assignment-puzzle-part-2/)

# クイズ

次のコードがあります。

```csharp
class Program
{
    static void Main()
    {
        object obj = GetObject(); // ★
        string str;
        if ((obj != null) && ((str = GetString()) != null)) // ◆
        {
            System.Console.WriteLine(obj + str); // ▲
        }
    }

    static object GetObject()
    {
        return "hello";
    }

    static string GetString()
    {
        return "goodbye";
    }
}
```

このコードは普通にコンパイルも通りますし動作しますが、★印をつけた7行目を次のように変更すると

```csharp
        dynamic obj = GetObject(); // ★ 変数の型をdynamicに変更
```

とたんに▲印をつけた9行目でコンパイルが通らなくなります。

```
Compilation error (line 9, col 44): Use of unassigned local variable 'str'
```

エラーメッセージは、`str`変数が初期化されていないというものです。
確かに、6行目で変数を定義したときは初期化していないのですが、◆印をつけた7行目の`if`文の条件式の中、`&&`演算子の右側で初期化しています。
`&&`の意味を考えると、9行目に到達するには`str`を初期化するコードも評価実行されるはずですし、実際最初の段階ではコンパイルできていたのですが、どうしてこうなるのでしょうか。

気になる人は[オンラインのPlaygroundで試してみてください](https://dotnetfiddle.net/lxHExk)。

# 解説

## `dynamic`型

C#は基本的には静的に型をチェックするタイプのプログラミング言語ですが、いくつか例外があります。その一つがこの`dynamic`型で、この型の値に対する操作、たとえばメソッド呼び出しやフィールド参照など、についてはその存在がコンパイル時にはチェックされず、実行時にしかチェックされなくなります。

それだけではなく、`dynamic`型の変数があったとすると、その変数が実際にはどんな型の値を保持しているかについて、仮定も追跡もせず、いつも不確定として扱われます。
今回のクイズのコードで言えば、`GetObject()`が`object`型（実体は`string`）を返すこと、`obj`変数はローカル変数なので、知らぬ間に外部から値が書き換えられたりしない[^1]こと、などは明白なのですが、それでも◆印をつけた7行目の`if`文の条件式においては、`obj`にどんな値が入っているかは実行してみるまでわからない、という風にコンパイラは扱います。
[^1]: `ref`を使って明示的に[参照渡し](https://ufcpp.net/study/csharp/sp_ref.html)をすれば書き換えることもできますが、それは意図的にやることですね。

それにしても、コンパイルが通らなくなるのは不思議ですね。`obj`変数に「なにか謎の型の値」が入っていた場合に`str`が初期化されなくなるようなことが起こり得るとでも言うのでしょうか。__実はその通り__なのです。そしてそこには、演算子オーバーロードが関係しています。

## nullチェックと演算子

◆印をつけた7行目の`if`文の条件式を見ると、`obj`変数と`null`を比較しています。

```csharp
(obj != null)
```

普通はこのようなコードでnullチェックができるのですが、`obj`の型によっては期待する動きにならない可能性もあるのです。それは、`obj`の型が__等値演算子 `==` と 非等値演算子 `!=` をオーバーロード定義して、独自の処理にしている場合__です。（`==`演算子と`!=`演算子は、かならずペアでオーバーロード定義する必要があります[^2]。）

[^2]: `Equals()`と`GetHashCode()`もオーバーライドしないとコンパイル時に警告が出ます。ですが今回は無視します。

たとえば以下のような`Weird`クラスがあるとします。

```csharp
class Weird
{
    public static bool operator ==(Weird left, Weird right)
    {
        return true;
    }
    public static bool operator !=(Weird left, Weird right)
    {
        return true;
    }
}
```
上のコードでは、`Weird`型同士に対する `==`演算も`!=`演算も、いつでも`true`を返します。ということは、

```csharp
Weird obj = null;
if (obj != null)
{
    Console.WriteLine(obj.ToString());
}
```

こんなコードを書くと、`obj`がnullでないことを確認したつもりが、実際はnullにもかかわらず条件式が真になってしまうので、`ToString()`メソッド呼び出しのところで`NullReferenceException`が投げられてしまいます！

ここで教訓の1つ目。

__等値演算子がオーバーロードされているときは、`null`と比較してもnullチェックにならないことがある。__

この教訓はあくまで、クラスの利用側で安全側に倒す方針を取るときのものです。クラスの定義側で等値演算子をオーバーロードするときは、`null`との比較がおかしなことになるようなコードを書くべきではありません！

とはいえ、C#を採用していることで有名なゲームプラットフォームである[Unity](https://unity3d.com/jp)では、[`UnityEngine.Object.Destroy()`メソッド](https://docs.unity3d.com/ja/current/ScriptReference/Object.Destroy.html)で破壊されたオブジェクトは、その後`null`との比較が`true`を返すようになります。

参考：[GameObjectやComponentをnullと比較するときの落し穴](https://qiita.com/RyotaMurohoshi/items/339d77d10b89198ce4fc)

そのような、比較演算子がオーバーロードされている（かもしれない）状況で、本当に`null`かどうかをチェックしたい場合は、以下のような選択肢があります[^7]。

[^7]: [岩永さんのコメント](https://twitter.com/ufcpp/status/1069019554909585408)を受けて編集しました。

- [パターンマッチ](https://docs.microsoft.com/ja-jp/dotnet/csharp/pattern-matching)でnullチェックをする
- [`System.Object.ReferenceEquals()`静的メソッド](https://docs.microsoft.com/ja-jp/dotnet/api/system.object.referenceequals)を呼び出す
- `object`型にアップキャストしてから比較演算子を使う

C# 7以降のパターンマッチでは、型パターンを使った場合`null`とはマッチしないので`null`でないことが確かめられます。また、`null`定数パターンを使った場合`null`にしかマッチしません。

`null`定数パターンでnullチェックをするコード例はこうなります。単純ですね。

```csharp
(obj is null)
```

さて、これで最初のコードの謎が解けた……と思ったら、そうではありません。

`!=`演算子がオーバーロードされているとしても、`true`を返すのであれば `&&` の右側が評価されて`str`は初期化されるはずだし、`false`を返すのであれば`if`文の条件式そのものが`false`になるので▲の印をつけた9行目は実行されないわけですから、コンパイルエラーになるのはおかしいのです。

では、等値演算子が`bool`型を返さないとしたら？

そんなことができるのか？というと、__できます__。演算子のオーバーロードでは、一部の単項演算子[^3]を除き、演算結果を独自の型にすることができるのです。たとえば、`Weird`クラスの`==`演算子と`!=`演算子を次のように書き直してみます。

[^3]: `true`演算子と`false`演算子は`bool`型しか返せません。`++`演算子と`--`演算子は元の型かその派生型しか返せません。

```csharp
class Weird
{
    public static CustomBoolean operator ==(Weird left, Weird right)
    {
        return new CustomBoolean();
    }
    public static CustomBoolean operator !=(Weird left, Weird right)
    {
        return new CustomBoolean();
    }
}
class CustomBoolean
{
}
```

このようにカスタムの`CustomBoolean`という型を返すように演算子を定義したとしても`Weird`クラス自体はコンパイルに通ります。

ですが、このままでは次の２つの問題があります。

- ◆印をつけた7行目の`if`文の条件式にあるような、 `CustomBoolean` 型と `bool` 型に対する `&&` 演算をどう定義するか
- もし `&&` 演算をしなかったら `if (obj != null) {}` の条件式の型は `CustomBoolean` になるが、`if` 文はどう動作するのか

`CustomBoolean`型から`bool`型に暗黙のキャストをする演算子を定義すれば、どちらの問題も起きません。ですがそれは、`==`や`!=`で`CustomBoolean`型を返し、即座に`bool`に変換する、ということになりますから、わざわざ独自の型を経由する意味がありません。`CustomBoolean`のような型を使うのであれば、__論理演算の間は`CustomBoolean`型の上で行い__、`if`文などの__条件式に使うところで真偽を確定させる__ような動きになっていてほしいわけです。ということで、`bool`型から`CustomBoolean`型に暗黙のキャストをする演算子を定義すれば、論理演算の対象が`CustomBoolean`型同士になりますので、後は`CustomBoolean`型のふるまいだけの問題です。

- `CustomBoolean`型同士の論理演算
- `if`文などの制御構文の条件式が`CustomBoolean`型のときの挙動

そして、C#はこの2つをカスタマイズする仕組みが用意されています。

なるほどなるほど、`str`変数が初期化されないケースを作れそうな気がしてきました。ですがそこに行く前に、どうして`CustomBoolean`みたいな型を定義できる仕組みが用意されているのか、それを説明しましょう。

## 論理演算と真偽チェックと演算子

`+` `-` `*` `/` の算術演算子をオーバーロード定義したいケースはいろいろ思いつくと思います。標準クラスライブラリの型でいえば`System.Numerics`名前空間に含まれる  `BigInteger` （任意精度整数）構造体や `Complex` （複素数）構造体などが算術演算子をオーバーロード定義することで、四則演算をやりやすくしています。

一方で、`!` `&` `&&` `|` `||` の論理演算子をオーバーロード定義したいケースにはどのようなものがあるというのでしょうか。ひとつには独自の型でビット演算を実現することがあると思いますが、その場合は `&&` や `||` は使いませんね。もうひとつは独自の型で論理演算を実現することで、この場合は`&&` や `||` が関係してきます。独自の型で論理演算をしたい代表的なケース（というより、唯一に近いケース）が __三値論理__ を扱うことです。

詳細は[Wikipedia](https://ja.wikipedia.org/wiki/3%E5%80%A4%E8%AB%96%E7%90%86)を読んでもらえたらと思いますが、ごく簡単に言うと、真偽値として「真(true)」「偽(false)」に加えて「不明(unknown)」という値を取れるようにした論理演算です。そして、論理演算 `!` `&` `|` はunknownを含む形で拡張されます。たとえば、`A`がunknownの時の`A & B`の結果は、`B`がfalseの時だけ`A & B`もfalseになりますが、それ以外の場合は`A & B`はunknownになります。

三値論理が使われる代表的な例がSQLです。例として`SELECT`文を考えます。`WHERE`句の条件式がNULLとの比較を含む場合、比較結果はtrueでもfalseでもないunknownとして扱われます。そして、`SELECT`文で選択される行は、`WHERE`句の条件式がtrueと評価される行だけです。つまり、論理演算の結果、条件式がunknownになる場合は、その行は選択されません。

SQLの場合は`WHERE`句や`HAVING`句や`CASE`式などが条件式の値を評価してtrueかどうか確かめます。C#の場合は、次の制御構文の中で、条件式の値を評価します。

* `if`文
* `else`文
* `while`文
* `do`文
* `for`文
* 三項条件演算子 `? :`
* 条件論理演算子 `&&` `||`

コンパイル時には条件式の型がチェックされますが、`bool`型、または暗黙的に`bool`型に変換可能な型の場合は普通にコンパイルが通ります。一方、`bool`型に変換できない型が条件式に使われている場合、真偽チェック専用の単項演算子である [true演算子](https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/true-operator) と [false演算子](https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/false-operator)がオーバーロード定義されているかをコンパイラーがチェックします。（`true`演算子と`false`演算子は、かならずペアでオーバーロード定義する必要があります。）この2つの演算子がオーバーロード定義されている型であれば、`bool`に変換することなく、制御構文の中で使用できます。

```csharp
class CustomBoolean
{
    public static bool operator true(CustomBoolean x)
    {
        // 条件式でtrueと評価できる場合はtrueを返す
    }

    public static bool operator false(CustomBoolean x)
    {
        // 条件式でfalseと評価できる場合はtrueを返す
    }
}
```

なぜ`bool`型に変換するのではなく、`true`演算子と`false`演算子を使うのか、ですが、上でも述べたように3値論理のunknownは「trueでもfalseでもない値」なのですから、それを表現できるようにするためです。`true`演算子と`false`演算子をペアで用意することで、`true`演算子の適用結果も、`false`演算子の適用結果も、どちらも`false`を返すような実装をすることができます。

……と書きましたが、上に挙げた制御構文の条件式では、実は1つを除いて他はすべてtrueのチェックしかしません（`true`演算子だけを適用する形にしかコンパイルされません）。__唯一例外的にfalseのチェックをする__（`false`演算子を適用する形にコンパイルされる）__のが、`&&` 条件論理演算子__なのです。

三項条件演算子 `? :` と 条件論理演算子 `&&` `||` は、直接オーバーロード定義することができません。`true`演算子や`false`演算子などを使った形にコンパイルされることがC#の言語仕様書に記載されています。言語仕様書には形式的な定義が書かれていますが、それを元に、三項条件演算子 `? :` と条件論理演算子 `&&` `||` で`CustomBoolean`のような型を使うとどんな感じにコンパイルされるのか、説明してみます[^4]。

[^4]: `if`文などの制御構文の方は細かい説明を省略しますが、`true`演算子の適用結果が`true`値を返すかどうかがチェックされます。

```csharp:三項条件演算子
CustomBoolean expr = GetCustomBoolean();

Foo foo = expr ? GetBar() : GetBaz();
// 以下、等価なコードのイメージ
// ただし本当は、演算子の適用をメソッド呼び出しの形で書くことはできない。
/*
Foo foo;
if (CustomBoolean.op_True(expr)) // true演算子の適用
{
    foo = GetBar();
}
else
{
    foo = GetBaz();
}
*/ 
```

```csharp:条件論理演算子&&
CustomBoolean expr = GetCustomBoolean() && GetAnotherCustomBoolean();
// 以下、等価なコードのイメージ
// ただし本当は、演算子の適用をメソッド呼び出しの形で書くことはできない。
/*
CustomBoolean expr;
CustomBoolean left = GetCustomBoolean();
if (CustomBoolean.op_False(expr)) // false演算子の適用
{
    expr = left;
}
else
{
    CustomBoolean right = GetAnotherCustomBoolean();
    expr = CustomBoolean.op_BitwiseAnd(left, right); // left & right
}
*/ 
```

```csharp:条件論理演算子||
CustomBoolean expr = GetCustomBoolean() || GetAnotherCustomBoolean();
// 以下、等価なコードのイメージ
// ただし本当は、演算子の適用をメソッド呼び出しの形で書くことはできない。
/*
CustomBoolean expr;
CustomBoolean left = GetCustomBoolean();
if (CustomBoolean.op_True(expr)) // true演算子の適用
{
    expr = left;
}
else
{
    CustomBoolean right = GetAnotherCustomBoolean();
    expr = CustomBoolean.op_BitwiseOr(left, right); // left | right
}
*/ 
```

上では3つの演算子の動きをコードで書きましたが、`&&` については言葉でも説明しておきましょう。「左項の評価結果がfalseと判断される場合（`false`演算子が`true`を返した場合）はショートサーキットで右項は評価されず、左項の値がそのまま式の値となる。そうでない場合（`false`演算子が`false`を返した場合）は右項も評価され、最終的に左項と右項の（`&`演算子オーバーロードによる）論理積が式の値となる。」です。

というわけで、`CustomBoolean`クラスのような独自の型に対して `&&` 演算子を適用するには、`true`演算子と`false`演算子に加えて、`&`演算子をオーバーロード定義しておく必要があります。同様に、`||`演算子を適用するには、`true`演算子と`false`演算子に加えて、`|`演算子をオーバーロード定義しておく必要があります。

さて、ここで問題です。もし、`CustomBoolean`クラスが`true`演算子と`false`演算子をオーバーロードして、どちらも常に`true`を返すようにしていたら、どんなことが起きるでしょうか？

```csharp
class CustomBoolean
{
    public static bool operator true(CustomBoolean x)
    {
        return true;
    }

    public static bool operator false(CustomBoolean x)
    {
        return true;
    }
}
```

もし上のようなむちゃくちゃなクラスを書いてしまうと、次のコードがひどいことになります！

```csharp
if (GetCustomBoolean() && GetAnotherCustomBoolean())
{
    DoSomething();
}
```

まず、`GetCustomBoolean()`が呼び出され、`CustomBoolean`型の値を受け取ります。次に、オーバーロードされた`false`演算子が呼び出されますが、これが`true`を返すため、`&&` 演算子の右項は実行されず、さっき受け取った`CustomBoolean`型の値がそのまま `if`文の条件式になります。ここで今度は`true`演算子が呼び出されますが、こちらも`true`を返します。そのため、`if`文直後のブロックが実行されます。__`&&`の右項が実行されなかったのに`if`直後のブロックが実行される__ことは、このようにして起こってしまうわけです[^5]。

[^5]: コンパイルを通すためには `&` 演算子もオーバーロード定義する必要があります。`&&` の右項は実行されないので、結果として`&`演算子も呼び出されませんが。

ここで教訓の2つ目。

__`&&` や `||` といった短絡評価する論理演算の意味を`true`と`false`の演算子オーバーロードでぶっ壊すことができてしまう。__

ちなみに、C#の言語設計チームが当初想定していた、カスタムの三値論理については、`bool?`型 （`Nullable<bool>`構造体）を使えば済むようになったため、あえて自分で定義する必要はほぼまったくありません。

# まとめ

さて、これで▲をつけた9行目でコンパイルが通らなくなったわけを説明する材料が出そろいました。

```csharp
class Program
{
    static void Main()
    {
        dynamic obj = GetObject(); // ★
        string str;
        if ((obj != null) && ((str = GetString()) != null)) // ◆
        {
            System.Console.WriteLine(obj + str); // ▲
        }
    }

    static object GetObject()
    {
        return "hello";
    }

    static string GetString()
    {
        return "goodbye";
    }
}
```

このようなコードでは、`true`演算子と`false`演算子がどちらも`true`を返すような`CustomBoolean`クラスと、`!=` 演算子が`CustomBoolean`型を返すような`Weird`クラスを使った場合、◆印をつけた7行目で`&&` の右項[^6]が実行されないため`str`変数が初期化されていないにもかかわらず、▲印をつけた9行目が実行されるというケースがありえてしまいます。

[^6]: `&&` 演算子の右項は`bool`型のままなので、`dynamic`ではなく実際に`Weird`のような型でコンパイルを通そうと思ったら、`bool`型を`CustomBoolean`クラスに変換する、暗黙的なキャスト演算子も定義する必要があります。

また、★印をつけた5行目を見れば、`obj`変数は`GetObject()`の戻り値が格納されることははっきりしています。ですが、変数の型を`dynamic`にしてしまうと、◆印をつけた7行目で`obj`変数が何を格納しているか、まったく想定できません。想定できないということは、それが`Weird`のようなクラスのインスタンスである可能性を排除できないということです。しかも、コード上はそのようなクラスを定義していなかったとしても、参照しているアセンブリや、リフレクションを使って動的にロードするアセンブリを考えれば、`Weird`みたいなクラスがどこにもまったく存在しないことまでは保証できません。

したがって、`obj`変数の型を`dynamic`に変えたとたん、▲印をつけた9行目でコンパイルエラーが起きるのは、コンパイラーに問題があるからではない、というわけです。

最後に教訓を2つ。

__予想もしないコンパイルエラーが出るからと言って、コンパイラーのバグとは限らない。__

__演算子オーバーロードは、意味論・用法を守って正しく使いましょう。__