xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

型スイッチ

F# などの関数型言語をやっている方にはおなじみのパターンマッチング。「10 年おせーよ!」と言われそうですが、C# 7 にもそんな待ちに待った (?) 機能が搭載されそうです。主に既存の is キーワードと switch キーワードを拡張した、型に対するマッチングになる見込みです。本家ではこの機能を Type Switch と呼んでいるようです。パターンマッチングについては以下にまとめられています。

全体計画の一部を実装

パターンマッチングとしては他にも match 式や let ステートメントなども検討されていますが、それらは C# 8 以降での追加になりそうです。詳細は以下を参照してください。

f:id:xin9le:20160513032103p:plain

is 拡張

C# 6 までの is キーワードは以下のように型判定に使われていました。

if (x is string)
{
    //--- 変数 x が string 型だった場合の処理
}

このように is の後ろに型を指定して判定するだけの機能でしたが、これが大幅に拡張されます。

定数マッチング

特定の値かどうかを判定できるようになります。等価演算子 (==) で良いのではないか?と思えるくらいには存在意義を見出せない...。

//--- 何か値があるとする
var x = 123;

//--- C# 6 まではこんな感じ
var a = x == 123;  //--- True
var b = x == 456;  //--- False
var c = x == 'a';  //--- False

//--- C# 7 ではこんな感じで判定できるように!
var a = x is 123;   //--- True
var b = x is 456;   //--- False
var c = x is 'a';   //--- False
var d = x is "123"; //--- Error!! : Cannot implicitly convert type 'string' to 'int'

型マッチング

特定の型かどうかの判定ができるようになります。...と言うと「それ今までと同じやん!」と思われるかもしれませんが、特定の型判定に加えて変数の生成を同時にやってくれます。以下のサンプルを見てください。

//--- 何か object 型に入った値があるとする
object x = "abc";

//--- C# 6 ではこう
var v = x as string;
if (v != null)
{
    //--- 変数 v に対する処理
}

//--- もしくはこう
if (x is string)
{
    var v = (string)x;
    //--- 変数 v に対する処理
}

//--- C# 7 からはこう書ける!
if (x is string v)
{
    //--- 変数 v に対する処理
}

見慣れない書き方に若干戸惑うかもしれませんが、以下のような動きをしています。

  1. xstring 型かどうかを判定
  2. string 型だった場合、string 型の変数 v に値を代入
  3. 評価結果を bool 型で返す

型マッチングの変数スコープ

先の変数 v のスコープは if 文のスコープのみです。なので、以下のように変数として評価結果を受ける場合は v にアクセスできません

object x = 123;
var r = x is string v;
Console.WriteLine(r);  //--- false
Console.WriteLine(v);  //--- 変数 v を参照できないのでコンパイルエラー!

型マッチングが便利なシーン

以下のような null 許容型の判定などで便利そうです。だいぶスッキリと書ける印象があります。

//--- C# 6 ではこう
int? v = x?.y?.z;
if (v.HasValue)
{
    var v = x.GetValueOrDefault();
    //--- v を使った何か
}

//--- C# 7 ではこう書ける!
//--- null のときの評価は当然 false
if (x?.y?.z is int v)
{
    //--- v を使った何か
}

型マッチングの型判定は厳密

これまでの is キーワードと同様、型マッチングの型判定は非常に厳密です。ですので暗黙的型変換などは期待してはいけません。

object x = 123;

Console.WriteLine(x is int);  //--- true
Console.WriteLine(x is long); //--- false

if (x is int  v1){ /* OK */ }
if (x is long v2){ /* NG */ }  //--- 暗黙的型変換は当然してくれません

逆コンパイル

is 式の判定がどのように行われているか、ILSpy で逆コンパイルして覗いてみましょう。以下のように展開されます。

//--- これを逆コンパイルにかけると...
static void Main()
{
    object x = "abc";
    if (x is string v1) Console.WriteLine(v1);
    if (x is int v2)    Console.WriteLine(v2);
}

//--- こう展開される
static void Main()
{
    object x = "abc";

    //--- 参照型の場合はそのまま as で判定
    string v1 = x as string;
    bool flag = v1 != null;
    if (flag)
        Console.WriteLine(v1);

    //--- 値型の場合は null 許容型として判定
    int? num = x as int?;
    int v2 = num.GetValueOrDefault();  //--- if 文の中で展開すればいいのでは?(と思ったり
    bool hasValue = num.HasValue;
    if (hasValue)
        Console.WriteLine(v2);
}

参照型かどうかの判定の場合はただの as キーワードで、値型かどうかの判定の場合は as キーワード + null 許容型で行われるみたいですね。

switch 拡張

is キーワードが拡張されたのと同様の機能が switch 文としても組み込まれます。書き方がちょっと変わっただけなので、is の書き方が分かれば難しくないですね!

object x = 123;
switch (x)
{
    case "abc":
        Console.WriteLine(x);
        break;

    case int v:
        Console.WriteLine(v);
        break;
   
    default:
        Console.WriteLine("default");
        break;
}

when 句による条件設定 (case guard)

C# 6 では 例外フィルター という catch 句の後ろに when 句を付ける条件分岐機能が追加されました。これと似た感じで、case 句の後ろに when 句を入れて条件分岐する「case guard」と呼ばれる機能が追加されます。以下のように使います。

object x = 123;
switch (x)
{
    case "abc":
        Console.WriteLine(x);
        break;

    case int v when 100 < v:  //--- v が 100 より大きいとき
        Console.WriteLine(v);
        break;

    case int v:  //--- v が 100 以下のとき
        Console.WriteLine(v);
        break;
   
    default:
        Console.WriteLine("default");
        break;
}

これでより細かい条件分岐を簡潔に書くことができるようになりましたね!

逆コンパイル

switch 文も逆コンパイルして展開結果を確認してみましょう。上のコードを展開すると以下のようになります。

object x = 123;
object obj = x;  //--- どーにも納得できないけど一度コピーが入ってる...
if (object.Equals(obj, "abc"))
{
    Console.WriteLine(x);
}
else
{
    int? num = obj as int?;
    int v = num.GetValueOrDefault();
    if (num.HasValue && 100 < v)  //--- when 句で指定した条件も入っている
    {
        Console.WriteLine(v);
    }
    else
    {
        num = (obj as int?);
        int v2 = num.GetValueOrDefault();  //--- これも if の中で展開して欲しい
        if (num.HasValue)
        {
            Console.WriteLine(v2);
        }
        else
        {
            Console.WriteLine("default");
        }
    }
}

if 文を使って上手くやっている感じですね。