条件の重複チェック

switch ステートメント/switch 式中に絶対に到達できない条件があるとき、 ある程度はコンパイル時に検知してコンパイル エラーにしてもらえます。

パターンを使った switch の条件は上から逐次判定なので、要するに、上の方に下にある条件の上位互換な条件があるとコンパイル エラーになります。

一番わかりやすいのは破棄パターンで、これは「何にでも一致するパターン」なので、その下に何かを書くとエラーになります。

int M(object obj) => obj switch
{
    _ => 0,
    string _ => 1,
};

当然ですが、全く同じ条件が2つ以上ある場合にも、1つ目以外には絶対に到達しないのでエラーになります。

int M(object obj) => obj switch
{
    string s => s.Length,
    string _ => 1,
};

ちなみに、whenだと重複チェックが漏れることがあります。 一方、同じような条件でも、再帰パターンを使うとチェックが働きやすいです。

int M1(object obj) => obj switch
{
    // when 句を使うと「同じ条件」判定ができなくなる。コンパイルできてしまう。
    string s when s.Length == 0 => 0,
    string s when s.Length == 0 => 1,
    _ => -1,
};
 
int M2(object obj) => obj switch
{
    // 同じことを再帰パターンでやるとちゃんと重複チェックが掛かる。2つ目でコンパイル エラーに。
    string { Length: 0 } => 0,
    string { Length: 0 } => 1,
    _ => -1,
};

また、bool には網羅性のチェックが掛かるので、例えば以下のコードはコンパイル エラーになります。

int M(bool a, bool b) => (a, b) switch
{
    (false, false) => 0,
    (true, false) => 1,
    (false, true) => 2,
    (true, true) => 3,
    // bool の場合上記4つ以外は絶対にないことがわかるので、この行でコンパイル エラーになる。
    _ => 4,
};

再帰パターンの利用例

型スイッチの用途」と同じ題材で、再帰パターンの利用例も挙げておきます。

使った題材は、数式を扱うようなクラスです。 要するに、例えば、「x×x+1」というような式を、以下のようなクラスで表します。

public abstract class Node
{
    public static readonly Node X = new Var();
    public static implicit operator Node(int value) => new Const(value);
    public static Node operator +(Node left, Node right) => new Add(left, right);
    public static Node operator *(Node left, Node right) => new Mul(left, right);
}
 
public class Var : Node { public override string ToString() => "x"; }
 
public class Const : Node
{
    public int Value { get; }
    public Const(int value) { Value = value; }
    public void Deconstruct(out int value) => value = Value;
    public override string ToString() => Value.ToString();
}
 
public class Add : Node
{
    public Node Left { get; }
    public Node Right { get; }
    public Add(Node left, Node right) => (Left, Right) = (left, right);
    public void Deconstruct(out Node left, out Node right) => (left, right) = (Left, Right);
    public override string ToString() => $"({Left.ToString()} + {Right.ToString()})";
}
 
public class Mul : Node
{
    public Node Left { get; }
    public Node Right { get; }
    public Mul(Node left, Node right) => (Left, Right) = (left, right);
    public void Deconstruct(out Node left, out Node right) => (left, right) = (Left, Right);
    public override string ToString() => $"{Left.ToString()} * {Right.ToString()}";
}

こいつに対して「式の簡約化」をやってみます。 要は、 「x+0xに、 x×1xに、 x×00に直す」みたいなやつ。

こういう処理は、switch式と位置パターンを使って以下のように書けます。 (コード全体: Expressions/Program.cs)

public static Node Simplify(this Node n)
    => n switch
{
    Add (var l, var r) => (l.Simplify(), r.Simplify()) switch
    {
        // 0 を足しても変わらない
        (Const(0), var r1) => r1,
        (var l1, Const(0)) => l1,
        // 他
        (var l1, var r1) => new Add(l1, r1)
    },
    Mul (var l, var r) => (l.Simplify(), r.Simplify()) switch
    {
        // 0 を掛けたら 0
        (Const(0) c, _) => c,
        (_, Const(0) c) => c,
        // 1 を掛けても変わらない
        (Const(1), var r1) => r1,
        (var l1, Const(1)) => l1,
        // 他
        (var l1, var r1) => new Mul(l1, r1)
    },
    _ => n
};

C# 7.3 までだと、この処理は以下のように書くことになります。

public static Node ClassicSimplify(this Node n)
{
    if (n is Add a)
    {
        var (l, r) = a;
        var l1 = l.Simplify();
        var r1 = r.Simplify();
 
        { if (l1 is Const c && c.Value == 0) return r1; }
        { if (r1 is Const c && c.Value == 0) return l1; }
        return new Add(l1, r1);
    }
    if (n is Mul m)
    {
        var (l, r) = m;
        var l1 = l.Simplify();
        var r1 = r.Simplify();
 
        {
            if (l1 is Const c)
            {
                if (c.Value == 0) return c;
                if (c.Value == 1) return r1;
            }
        }
        {
            if (r1 is Const c)
            {
                if (c.Value == 0) return c;
                if (c.Value == 1) return l1;
            }
        }
        return new Mul(l1, r1);
    }
    return n;
}

更新履歴

ブログ