目次

概要

Ver. 7

C# 7.0で、is演算子switchステートメントcaseが拡張されました。

C# 6.0 以前では以下のような仕様でした。

  • is演算子 … x is T と言うように、型の判定だけができた
  • switchステートメントのcasecase の後ろには定数だけが指定で来た

これに対して、C# 7.0 以降では、iscaseの後ろに「パターン」を指定できます。 「パターン」の詳細については次項で別途説明する予定ですが、 簡単に概要だけ表にすると以下のようなものがあります。

パターン バージョン 概要
型パターン C# 7.0 型の判定 int istring s
定数パターン C# 7.0 定数との比較 null1
discard C# 7.0 何にでもマッチ・無視 _
var C# 7.0 何にでもマッチ・変数で受け取り var x
位置パターン C# 8.0 分解と同じ要領で、再帰的にマッチングする (1, var i, _)
プロパティ パターン C# 8.0 プロパティに対して再帰的にマッチングする { A: 1, B: var i }

C# 7.0 時点では「型パターン」が主だった機能だったため、 isswitchの拡張を指して「型スイッチ」(type switch)と呼ばれたりもしました。

本項では、まずはisswitchがC# 6.0以前と比べてどう変わったかについて焦点を当てます。 例なども、主に型パターン(C# 7.0)で説明していきます。 パターン自体の詳細については次項の「パターン マッチング」を参照してください。

is演算子の拡張

C# 7では、is演算子で以下のような書き方ができるようになりました。

型を調べたい変数 is  新しい変数

(正確に言うとisの後ろに新たに書けるようになったのは「パターン」で、 これはそのうちの「型パターン」と呼ばれるものです。)

C# 6以前のis演算子は少し使い勝手が悪い面がありました。型の一致を判定するだけならいいんですが、 型変換も絡むといまいちです。

例えば、以下のように型を判定するだけならis演算子の出番です。

// 型判定のみなら、これまでの is 演算子でも十分
if (obj is string) Console.WriteLine("string");

ところが、型を判定したうえでダウンキャストしたいという場面では、以下のように、「2度手間」になって、コード量的にも実行効率的にもよくないです。

// 型変換もしたい
if (obj is string)
{
    var s = (string)obj;
    //↑ isとキャストで2つの別命令を使う。二重処理になってるだけで無駄
    Console.WriteLine("string #" + s.Length);
}

結局、以下のように、as演算子を使うことが推奨されます。

// 結局、as 演算子 + null チェックを使うことになる
var s = obj as string;
if (s != null)
{
    Console.WriteLine("string #" + s.Length);
}

これに対して、C# 7では、is演算子で以下のような書き方ができるようになりました。

// C# 7での新しい書き方
if (obj is string s)
{
    Console.WriteLine("string #" + s.Length);
}

挙動的には、先ほどのas演算子を使ったものとまったく同じ挙動になります。 is演算子で型を判定しつつ(boolの戻り値を返しつつ)、その型への変換結果を新しい変数で受け取れます。

is演算子で宣言された変数のスコープ

is演算子の拡張によって、式の中で変数宣言ができるようになりました。 そこで問題になるのはこの変数のスコープです。

概ね、「その式を含むブロック内」と考えていいんですが、ifwhileなどの中で使ったときなど、いくつか特殊な場合があります。 詳細については「式の中で変数宣言」を参照してください。

is演算子によるnullチェック

元々のis演算子の仕様でもあるんですが、nullには型がなくて常にisに失敗します(falseを返す)。

string x = null;

if (x is string)
{
    // x の変数の型は string なのに、is string は false
    // is 演算子は変数の実行時の中身を見る & null には型がない
    Console.WriteLine("ここは絶対通らない");
}

この仕様は、C# 7からの新しい構文でも引き継いでいて、nullじゃないときだけだけ何かの処理をしたいときに使えます。 と言っても、参照型の場合にはあまり使い道はありませんが、以下のような書き方ができます。

static void F(string nullable)
{
    if (nullable is string nonNull)
    {
        // nonNull には絶対に null が入らない
        // nullable をそのまま使っても、if の結果、null じゃない保証があるのであまり意味はないけども
        Console.WriteLine(nonNull.Length);
    }
}

この書き方が役に立つのは、値型とnull許容型を使う場合でしょう。 例えばC# 6以前だと、以下のような書き方になります。

static void F(int? x)
{
    // C# 6以前の書き方
    if (x.HasValue)
    {
        // この「.GetValueOrDefault()」をいちいち書くのが結構うっとおしい
        // x * x だと、(x.HasValue & x.HasValue) ? (int?)(x.GetValueOrDefault() * x.GetValueOrDefault()) : null みたいなコードに展開されてしまう
        int n = x.GetValueOrDefault();
        Console.WriteLine(n * n);
    }
}

これが、C# 7で以下のように書けるようになります。

static void F(int? x)
{
    if (x is int n)
    {
        Console.WriteLine(n * n);
    }
}

余談: 変数の意味を変えない

プログラミング言語によっては、以下のように、is演算子で型を判定した後には、自動的にその型扱いしてくれる言語もあります。

static void F(object obj)
{
    if (obj is string)
    {
        // この中では obj を string 扱いできる言語がある
        // C# ではコンパイル エラー
        Console.WriteLine("string #" + obj.Length);
    }
    else if (obj is int)
    {
        // 同上、int 扱いできる言語がある
        // C# ではコンパイル エラー
        Console.WriteLine("int " + (obj * obj));
    }
}

C# では、こういう、「objectだと思っていたものが一定範囲でだけ別の型になる」というようなことはやらない方針です。

また、以下のように、同名の別変数を導入できる言語もありますが、こちらもC#では認めていません。

static void F(object x)
{
    if (x is string x)
    {
        // 引数の x とは別に、is 演算子で別の「x」を導入できる言語もある
        // C# ではコンパイル エラー
        Console.WriteLine("string #" + x.Length);
    }
}

C#では、変数はスコープ内で意味不変(invariant meaning)であるべきという方針を持っています。 上記の2つの例では、objxが部分的に(ifの中でだけ)別の意味になるので、C#としては認めたくないものになります。

更新履歴

ブログ