概要
Ver. 7
C# 7.0で、is
演算子やswitch
ステートメントのcase
が拡張されました。
C# 6.0 以前では以下のような仕様でした。
is
演算子 …x is T
と言うように、型の判定だけができたswitch
ステートメントのcase
…case
の後ろには定数だけが指定で来た
これに対して、C# 7.0 以降では、is
、case
の後ろに「パターン」を指定できます。
「パターン」の詳細については次項で別途説明する予定ですが、
簡単に概要だけ表にすると以下のようなものがあります。
パターン | バージョン | 概要 | 例 |
---|---|---|---|
型パターン | C# 7.0 | 型の判定 | int i 、string s |
定数パターン | C# 7.0 | 定数との比較 | null 、1 |
var パターン | C# 7.0 | 何にでもマッチ・変数で受け取り | var x |
破棄パターン | C# 8.0 | 何にでもマッチ・無視 | _ |
位置パターン | C# 8.0 | 分解と同じ要領で、再帰的にマッチングする | (1, var i, _) |
プロパティ パターン | C# 8.0 | プロパティに対して再帰的にマッチングする | { A: 1, B: var i } |
C# 7.0 時点では「型パターン」が主だった機能だったため、
is
やswitch
の拡張を指して「型スイッチ」(type switch)と呼ばれたりもしました。
本項では、まずはis
やswitch
が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
演算子の拡張によって、式の中で変数宣言ができるようになりました。
そこで問題になるのはこの変数のスコープです。
概ね、「その式を含むブロック内」と考えていいんですが、if
やwhile
などの中で使ったときなど、いくつか特殊な場合があります。
詳細については「式の中で変数宣言」を参照してください。
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);
}
}
ただ、1つ注意が必要なのは、is var
という似て非なる構文がある点です。
is var
(var
パターンと言って、is T
とは別扱い)を使った場合、nullチェックはされません。
var
は何でも受け取れる構文で、null も受け付けます。
ちなみに、C# 8.0 では、再帰パターンが暗黙的に null チェックも含んでいることを使って、手短に null チェックもできます (参考: 非 null マッチング)。
string s = null;
// 型を明示した場合、null にマッチしない
if (s is string) Console.WriteLine("ここは通らない");
// var パターンは何にでも(null 含む)マッチする
if (s is var _) Console.WriteLine("ここは通る");
// 再帰パターンで型を省略すると null チェックも含む
if (s is { }) Console.WriteLine("ここは通らない");
余談: 変数の意味を変えない
プログラミング言語によっては、以下のように、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つの例では、obj
やx
が部分的に(if
の中でだけ)別の意味になるので、C#としては認めたくないものになります。