C# 関数型プログラミングの詳細 (15) パターン マッチング

[C# シリーズ経由の LINQ]

[C# 関数型プログラミングの詳細シリーズ]

最新バージョン:https://weblogs.asp.net/dixin/functional-csharp-pattern-matching

パターン マッチングは、関数型言語の一般的な機能です。 C# 7.0 では、パターンとしての定数値とパターンとしての型を含む基本的なパターン マッチングが導入され、C# 7.1 ではパターン マッチングでジェネリックがサポートされます。

is 式とのパターン マッチング

C# 7.0 より前では、インスタンスが指定された型と互換性があるかどうかをテストするために、インスタンス is Type 式で is キーワードが使用されていました。 C# 7.0 以降、null、定数値、列挙型を含む定数パターンをテストできます:

internal static partial class PatternMatching
{
    internal static void IsConstantValue(object @object)
    {
        // Type test:
        bool test1 = @object is string;
        // Constant pattern test:
        bool test5 = @object is null; // Compiled to: @object == null
        bool test6 = @object is default; // Compiled to: @object == null
        bool test2 = @object is int.MinValue; // Compiled to: object.Equals(int.MinValue, @object)
        bool test3 = @object is DayOfWeek.Monday; // Compiled to: object.Equals(DayOfWeek.Monday, @object)
        bool test4 = @object is "test"; // Compiled to: object.Equals("test", @object)
    }
}

null テストの is 式は、単に null チェックにコンパイルされます。他のケースは object.Equal 静的メソッド呼び出しにコンパイルされます。ここで、定数値は最初の引数であり、テストされるインスタンスは 2 番目の引数です。内部的に、object.Equals は最初にいくつかのチェックを行い、次に最初の引数の Equals インスタンス メソッドを呼び出すことができます:

namespace System
{
    [Serializable]
    public class Object
    {
        public static bool Equals(object objA, object objB) =>
            objA == objB || (objA != null && objB != null && objA.Equals(objB));

        public virtual bool Equals(object obj) =>
            RuntimeHelpers.Equals(this, obj);

        // Other members.
    }
}

C# 7.0 コンパイラの初期のバージョンは、テスト対象のインスタンスを object.Equals 呼び出しの最初の引数として取り、定数値を 2 番目の引数として取ります。これには問題が発生する可能性があります。このように、生成された static object.Equals は、テスト対象インスタンスの Equals インスタンス メソッドを呼び出します。テスト対象のインスタンスは任意のカスタム型にすることができ、その Equals インスタンス メソッドは任意のカスタム実装でオーバーライドできるためです。 C# 7.0 GA リリースでは、定数値を object.Equals の最初の引数にすることでこれが修正され、より予測可能な動作を持つ定数値の Equals インスタンス メソッドを呼び出すことができるようになりました。

パターンは型の場合もあり、その後にその型のパターン変数が続きます:

internal static void IsReferenceType(object @object)
{
    if (@object is Uri uri)
    {
        uri.AbsoluteUri.WriteLine();
    }
}

上記のパターンの型は参照型 (クラス) であるため、is 式は型変換と null チェックとしてコンパイルされます:

internal static void CompiledIsReferenceType(object @object)
{
    Uri uri = @object as Uri;
    if (uri != null)
    {
        uri.AbsoluteUri.WriteLine();
    }
}

このシンタックス シュガーは、値の型にも機能します:

internal static void IsValueType(object @object)
{
    if (@object is DateTime dateTime)
    {
        dateTime.ToString("o").WriteLine();
    }
}

as 演算子は値型には使用できません。型キャスト (ValueType) インスタンスは機能しますが、キャストが失敗すると例外がスローされます。したがって、値型のパターン マッチングは、as 演算子と HasValue チェックを使用して null 許容値型変換にコンパイルされます。

internal static void CompiledIsValueType(object @object)
{
    DateTime? nullableDateTime = @object as DateTime?;
    DateTime dateTime = nullableDateTime.GetValueOrDefault();
    if (nullableDateTime.HasValue)
    {
        dateTime.ToString("o").WriteLine();
    }
}

追加の条件でパターン マッチングを使用することも一般的です。

internal static void IsWithCondition(object @object)
{
    if (@object is string @string && TimeSpan.TryParse(@string, out TimeSpan timeSpan))
    {
        timeSpan.TotalMilliseconds.WriteLine();
    }
}

コンパイル後、null チェックに条件が追加されます:

internal static void CompiledIsWithCondition(object @object)
{
    string @string = @object as string;
    if (@string != null && TimeSpan.TryParse(@string, out TimeSpan timeSpan))
    {
        timeSpan.TotalMilliseconds.WriteLine();
    }
}

前述のデータ型は、オブジェクトの Equals メソッドをオーバーライドします:

internal partial class Data : IEquatable<Data>
{
    public override bool Equals(object obj)
    {
        return obj is Data && this.Equals((Data)obj);
    }

    public bool Equals(Data other) // Member of IEquatable<T>.
    {
        return this.value == other.value;
    }
}

従来の構文では、オブジェクト パラメータの型が 2 回検出されていました。 .NET Framework では、コード分析ツールはこれに対して警告 CA1800 を発行します。パラメーターである 'obj' は、メソッド 'Data.Equals(object)' で型 'Data' に複数回キャストされます。冗長な castclass 命令を排除するために、'as' 演算子または直接キャストの結果をキャッシュします。新しい構文を使用すると、警告なしで次のように簡略化できます:

internal partial class Data : IEquatable<Data>
{
    public override bool Equals(object obj) => 
        obj is Data data && this.Equals(data);
}

C# 7.1 は、パターン マッチングでジェネリックのオープン型をサポートしています:

internal static void OpenType<T1, T2>(object @object, T1 open1)
{
    if (@object is T1 open) { }
    if (open1 is Uri uri) { }
    if (open1 is T2 open2) { }
}

var キーワードは、任意のタイプのパターンにすることができます:

internal static void IsType(object @object)
{
    if (@object is var match)
    {
        object.ReferenceEquals(@object, match).WriteLine();
    }
}

var パターン マッチングは常に機能するため、デバッグ ビルドで true にコンパイルされます:

internal static void CompiledIsAnyType(object @object)
{
    object match = @object;
    if (true)
    {
        object.ReferenceEquals(@object, match).WriteLine();
    }
}

リリース ビルドでは、上記の if (true) テストは単純に削除されます。

switch ステートメントによるパターン マッチング

C# 7.0 より前では、switch ステートメントは文字列、整数型 (bool、byte、char、int、long など)、および列挙のみをサポートしていました。ケース ラベルは定数値のみをサポートします。 C# 7.0 以降、switch は任意の型をサポートし、case ラベルは定数値または型のパターン マッチングをサポートします。パターン マッチングの追加条件は、when 句で指定できます。次の例では、オブジェクトを DateTime に変換しようとしています:

internal static DateTime ToDateTime(object @object)
{
    switch (@object)
    {
        // Match constant @object.
        case null:
            throw new ArgumentNullException(nameof(@object));
        // Match value type.
        case DateTime dateTIme:
            return dateTIme;
        // Match value type with condition.
        case long ticks when ticks >= 0:
            return new DateTime(ticks);
        // Match reference type with condition.
        case string @string when DateTime.TryParse(@string, out DateTime dateTime):
            return dateTime;
        // Match reference type with condition.
        case int[] date when date.Length == 3 && date[0] > 0 && date[1] > 0 && date[2] > 0:
            return new DateTime(year: date[0], month: date[1], day: date[2]);
        // Match reference type.
        case IConvertible convertible:
            return convertible.ToDateTime(provider: null);
        case var _: // default:
            throw new ArgumentOutOfRangeException(nameof(@object));
    }
}

任意の型パターンを持つ最後のセクションは、常に一致するため、既定のセクションと同等です。各パターン マッチングは、式と同様の方法でコンパイルされます。

internal static DateTime CompiledToDateTime(object @object)
{
    // case null:
    if (@object == null)
    {
        throw new ArgumentNullException("@object");
    }

    // case DateTime dateTIme:
    DateTime? nullableDateTime = @object as DateTime?;
    DateTime dateTime = nullableDateTime.GetValueOrDefault();
    if (nullableDateTime.HasValue)
    {
        return dateTime;
    }

    // case long ticks
    long? nullableInt64 = @object as long?;
    long ticks = nullableInt64.GetValueOrDefault();
    // when ticks >= 0:
    if (nullableInt64.HasValue && ticks >= 0L)
    {
        return new DateTime(ticks);
    }

    // case string text 
    string @string = @object as string;
    // when DateTime.TryParse(text, out DateTime dateTime):
    if (@string != null && DateTime.TryParse(@string, out DateTime parsedDateTime))
    {
        return parsedDateTime;
    }

    // case int[] date
    int[] date = @object as int[];
    // when date.Length == 3 && date[0] >= 0 && date[1] >= 0 && date[2] >= 0:
    if (date != null && date.Length == 3 && date[0] >= 0 && date[1] >= 0 && date[2] >= 0)
    {
        return new DateTime(date[0], date[1], date[2]);
    }

    // case IConvertible convertible:
    IConvertible convertible = @object as IConvertible;
    if (convertible != null)
    {
        return convertible.ToDateTime(null);
    }

    // case var _:
    // or
    // default:
    throw new ArgumentOutOfRangeException("@object");
}