これまで(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 型には falsetrue しかないはずなのにこれはおかしいと言われ続けていたんですが、C# 8.0 では default 句が要らなくなるというか、default 句を絶対に通らなくなるよう、コード生成の仕方を変更するみたいです。

今日はこの辺りの、要するに「false でも true でもない bool 値」の話。

サンプルコード: BoolExhaustiveness

bool とは

ドキュメント上

まず、ドキュメント上、bool がどうなっているかというと…

大体は2つの値だけを取れる型として説明されています。

実装上: Boolean 構造体

その Boolean 構造体(System 名前空間)の内部実装がどうなっているかというと、

  • 1バイトの構造体
  • true の内部表現は 1
  • false の内部表現は 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 falsecase true があれば default 要らないだろ」と言われ続け、 そのたびに「内部的に false でも true でもない値があり得るから」という回答が返って来続けていたんですが。

この度、「ドキュメント上も 『truefalse の2値』と明記されているんだから、それ以外の値を想定して非効率なコードを生成するのはおかしいだろ」という突っ込みがあって、「それは確かに」的な空気になったみたいです。

また、C# 8.0 では switchも入るので、網羅性のチェック(「truefalse で全パターン網羅している」という判定)をしたい需要が高まったので、ついに折れて、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 以上にした場合だけ新しい挙動になる」に変更されるはずです。