これまで(C# 7.3 まで)、C# の switch ステートメントで bool 型を使う場合、以下のように、default 句が必須になることが多々ありました。
static int X(bool b)
{
    switch (b)
    {
        case false: return 0;
        case true: return 1;
        default: return -1;
    }
}
bool 型には false と true しかないはずなのにこれはおかしいと言われ続けていたんですが、C# 8.0 では default 句が要らなくなるというか、default 句を絶対に通らなくなるよう、コード生成の仕方を変更するみたいです。
今日はこの辺りの、要するに「false でも true でもない bool 値」の話。
サンプルコード: BoolExhaustiveness
bool とは
ドキュメント上
まず、ドキュメント上、bool がどうなっているかというと…
- 
C# Language Reference での 
boolの説明System.Boolean型のエイリアスで、真偽値、すなわち、trueかfalseを格納できる
 - 
Boolean構造体の説明trueかfalseのいずれかの2値を取れる型
 
大体は2つの値だけを取れる型として説明されています。
実装上: Boolean 構造体
その Boolean 構造体(System 名前空間)の内部実装がどうなっているかというと、
- 1バイトの構造体
 trueの内部表現は 1falseの内部表現は 0
です。 1バイトだけども0と1しか必要としないので、残り254個の値は基本的には使われません。
0 でも 1 でもない bool を作る
普通にリテラルの true, false や、== などの条件式から bool 値を得る限り、本当に0と1以外の値は発生しません。
ただ、C# は unsafe な手段を使って任意に値を書き換えれちゃうので、無理やりやると 0 でも 1 でもない bool 値を作れます。
具体的にはいくつか書き方があるんですが、1つ目は素直にポインターを使うもの。
unsafe bool toBool(byte b) => *((bool*)&b);
Console.WriteLine(toBool(2));
もう1つは、Unsafe クラスを使う書き方。
これもまあ、書き方が違うだけでポインターと大差ないです。
bool toBool(byte b) => Unsafe.As<byte, bool>(ref b);
Console.WriteLine(toBool(2));
最後に、StructLayout を使う(C 言語の union 風な使い方する)方法。
LayoutKind.Explicit は、ポインター並みに変なことができちゃう機能なので、
そもそも unsafe コードなしで使えること自体が疑問視されていたりもします。
要するに、実質 unsafe。
static void Main()
{
    bool toBool(byte b)
    {
        Union u = default;
        u.Byte = b;
        return u.Boolean;
    }
    Console.WriteLine(toBool(2));
}
[StructLayout(LayoutKind.Explicit)]
private struct Union
{
    [FieldOffset(0)]
    public byte Byte;
    [FieldOffset(0)]
    public bool Boolean;
}
0 でも 1 でもない bool を使うとどうなるか
x86 などの CPU では、条件分岐命令が以下のような方法で実現されています。
- 直前の命令の結果が 0 になったら立つフラグが CPU 内に存在する
 - そのフラグを見て分岐する
 
要するに、「0 かどうか」しか見ません。 この意味では、「true とは 0 以外の全ての値を指す」と言えます。
C# の if ステートメント
.NET の中間言語もそういう挙動をします。 brtrue 命令ってのを持ってるんですが、 こいつは「value が 0 でなければ分岐」という挙動。
C# の if ステートメントはこの命令(もしくはその逆の brfalse)に変換されるので、
「0 以外の値」は全て true 扱いになります。
実際、前述の方法で作った「中身が2のbool値」を if に渡すと true 側に分岐します。
using System;
class Pointer
{
    static void Main()
    {
        unsafe bool toBool(byte b) => *((bool*)&b);
        Branch(false);     // if (false)
        Branch(true);      // if (true)
        Branch(toBool(2)); // if (true)
    }
    static void Branch(bool b)
    {
        if (b) Console.WriteLine("if (true)");
        else Console.WriteLine("if (false)");
    }
}
if (false)
if (true)
if (true)
C# 7.3 までの switch ステートメント
問題はここからなんですが…
if ステートメントとは違って、(C# 7.3 までの) switch ステートメントは中身の値を見ます。
すなわち、普通の true と、「中身が2のbool値」は別の値という扱い。
これが、冒頭のコードで default 句が必須になる理由です。
実際、case true を通らないようなコードを書けます。
static void Main()
{
    // 0 → false
    // 1 → true
    // それ以外 → if (b) は通るんだけど、switch (b) { case true: } は通らない(C# 7.3 までは)変な値になる。
    for (byte i = 0; i < 3; i++)
    {
        Console.WriteLine($"value = {i}");
        Branch(Pointer(i));
        Branch(UnsafeAs(i));
        Branch(UnionStruct(i));
    }
}
/// <summary>
/// false (0) の時は何も表示されない。
/// true (1) の時は if(b) switch(b) の両方が表示される。
/// 「それ以外の値」を作って渡すと、if(b) だけが表示される。
/// </summary>
static void Branch(bool b)
{
    if (b) Console.WriteLine("    if(b)");
    switch (b) { case true: Console.WriteLine("    switch(b)"); break; }
}
型 switch
ちなみにこの「中身の値を見て分岐」挙動は、case が全部定数の場合(= 古き良き昔からある switch) の場合だけの挙動です。
C# 7.0 から入った、パターン マッチングを使った switch(いやゆる「型 switch」)の場合には brtrue 命令が使われるようになって、if ステートメントと同じ挙動になります。
using System;
class TypeSwitch
{
    static void Main()
    {
        Branch(0);
        Branch(1);
        Branch(2);
    }
    static unsafe void Branch(byte x)
    {
        var b = *((bool*)&x);
        Console.WriteLine($"value = {x}");
        Console.Write("    traditional switch: ");
        switch (b)
        {
            case false:
                Console.WriteLine("false");
                break;
            case true:
                Console.WriteLine("true");
                break;
            default:
                // 0でも1でもないbool値の時にここに来る
                Console.WriteLine("other");
                break;
        }
        Console.Write("    type switch: ");
        switch (b)
        {
            case false when true:
                Console.WriteLine("false");
                break;
            case true:
                Console.WriteLine("true");
                break;
            default:
                // 絶対ここは通らない
                Console.WriteLine("other");
                break;
        }
    }
}
value = 0
    traditional switch: false
    type switch: false
value = 1
    traditional switch: true
    type switch: true
value = 2
    traditional switch: other
    type switch: true
マーシャリング
ちなみに、P/Invokeを使う際には、マーシャリング時に「0でも1でもないbool値」をtrue(内部的に1のbool値)に置き換える処理が掛かるみたいです。
例えば、以下のような Rust コードを lib.dll 中で定義しておいて、
#[no_mangle]
pub extern fn id(x: i8) -> i8 { x }
これを C# 側から以下のように呼び出します。
using System;
using System.Runtime.InteropServices;
class Program
{
    static void Main(string[] args)
    {
        // 素通し。当然、2。
        byte a = Id(2);
        Console.WriteLine(a);
        // 素通しじゃなくて、bool で値を受け取り。true。
        bool b = ToBool(2);
        Console.WriteLine(b);
        unsafe
        {
            // 内部表現を見てみると、1 になってる。
            byte b1 = *(byte*)&b;
            Console.WriteLine(b1);
        }
    }
    /// <summary>
    /// rust 側の id 関数は i8 を素通しするだけ。
    /// それを DllImport で呼んでるので、このメソッドも素通し。
    /// </summary>
    [DllImport("lib.dll", EntryPoint = "id")]
    private static extern byte Id(byte x);
    /// <summary>
    /// マーシャリングで、byte な戻り値を bool で受け取ることができる。
    /// ただ、この場合、素通しではなくて、ちゃんと 戻り値 != 0 で bool に変換されているみたい。
    /// </summary>
    [DllImport("lib.dll", EntryPoint = "id")]
    private static extern bool ToBool(byte x);
}
id関数の戻り値は i8 (C# でいう sbyte)ですが、マーシャリング時に bool への変換をしてくれます。
変換の仕方は、!= 0 になっているみたいで、「0 でない値」だったら普通の true (内部的に1のbool値)が返ってきます。
C# 8.0 での switch ステートメントの変更
まあ、要するに、switch ステートメントだけがきもいです。
たびたび「case false と case true があれば default 要らないだろ」と言われ続け、
そのたびに「内部的に false でも true でもない値があり得るから」という回答が返って来続けていたんですが。
この度、「ドキュメント上も 『true と false の2値』と明記されているんだから、それ以外の値を想定して非効率なコードを生成するのはおかしいだろ」という突っ込みがあって、「それは確かに」的な空気になったみたいです。
また、C# 8.0 では switch 式も入るので、網羅性のチェック(「true と false で全パターン網羅している」という判定)をしたい需要が高まったので、ついに折れて、bool に対する switch の挙動を変えることにしたみたいです。
using System;
class Program
{
    static void Main()
    {
        Console.WriteLine(X(false)); // -1
        Console.WriteLine(X(true)); // 1
        unsafe
        {
            byte x = 2;
            bool y = *(bool*)&x;
            Console.WriteLine(X(y)); // C# 7.0 までは 0 だった。C# 8.0 で 1 になるように。
        }
    }
    static int X(bool b)
    {
        switch (b)
        {
            case false: return -1;
            case true: return 1;
            default: return 0;     // C# 7.0 までは何も言われなかった。C# 8.0 で「到達できないコード」警告出るように。
        }
    }
}
内部的には if 相当のコードへの置き換えです。
ちなみに、Visual Studio 2019 Preview 2だと、「LangVersion を 7.3 以下にしてても新しい方の挙動になってしまう」というバグがあったりします。 バグ認定はされていて、正式版までには「C# 8.0 以上にした場合だけ新しい挙動になる」に変更されるはずです。
