Pattern-Matching with Generics は必要なのか？

## はじめに
パターンマッチングの補完機能として C# 7.1 で追加された [_pattern-matching with generics_](https://qiita.com/inasync/items/6ffa2364c3b42442c6a2#4-pattern-matching-with-generics)。

始めは「型パラメーターの変数もマッチングできた方が自然だよね」と思いましたが、そもそもこのジェネリックでのパターンマッチング、どこで使うのでしょうか？
ユースケースは様々で正解はないと思いますが、個人的には**コードの複雑度を上げるだけ**と考えるのでお勧めしません。

この記事では機能追加のあらましとお勧めしない理由を記します。


## なぜ機能は追加されたのか？
[csharp-7.1/generics-pattern-match.md](https://github.com/dotnet/csharplang/blob/master/proposals/csharp-7.1/generics-pattern-match.md#motivation) には次のように書かれています。

> ## Motivation
Cases where pattern-matching should "obviously" be permitted currently fail to compile. See, for example, https://github.com/dotnet/roslyn/issues/16195.

「"明らかに" 許可されるべきケース」とありますが、その根拠はここにはありません。
続けて例示されている [dotnet/roslyn#16195](https://github.com/dotnet/roslyn/issues/16195) を読んでみましょう。

> Roslyn is recommending pattern matching for the following code with **IDE0019**. Applying the code fix to the following code results in the compilation error above.
> 
```csharp
var keepalive = packet as KeepalivePacket;
if (keepalive != null)
{
    // Do stuff with keepalive
}
```

上記のコードパターンの際に、Roslyn はパターンマッチングの使用を推奨（**IDE0019**）するようになっていますが、実際に適用すると C# 7.0 ではコンパイルエラーになります。これが機能追加の理由のようです。

```csharp:IDE0019適用後
if (packet is KeepalivePacket keepalive) {  // compile error
    // Do stuff with keepalive
}
```

ところで issue は以下のように続いています。
> Does **IDE0019** need to be updated to not apply when the variable/parameter is generic?

このようなケースでの Roslyn によるパターンマッチングの推奨を止める事も一案として問いかけています。
この対応でも良かったと思うのですが、**IDE0019** の件はこれ以降では触れられていません。なんでや。

この機能の pull-request [dotnet/roslyn#18784](https://github.com/dotnet/roslyn/pull/18784#issuecomment-295984942) に下記がコメントされています。

> @jaredpar Please look at this and determine if it is suitable for 15.3. A number of customers have complained, and the fix is small.

ジェネリック対応は、多くの顧客が望んだ結果なんですね（ﾆｯｺﾘ


## 原因は何なのか？
コンパイラは _type pattern_ の検証時、オペランドとの間に[変換](https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/language-specification/conversions)が無い場合に `ERR_PatternWrongType` としていました。しかしオペランドと _type pattern_ の何れかががオープン型[^1]の場合には変換が存在しないので、このケースに当たってしまうという訳です。

[^1]: 大雑把に言えば、型パラメーターを包含している型。e.g. `IEnumerable<T>`。オープン型以外は全てクローズ型。型引数が与えられた `IEnumerable<int>` はクローズ型。[Types | Microsoft Docs > Open and closed types](https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/language-specification/types#open-and-closed-types)

この動作はプログラムのバグではなく仕様漏れに相当するので、仕様に追記する形で修正されました。
[csharplang/proposals/csharp-7.1/generics-pattern-match.md > Detailed design](https://github.com/dotnet/csharplang/blob/master/proposals/csharp-7.1/generics-pattern-match.md#detailed-design)
> ## Detailed design
We change the paragraph in the pattern-matching specification (the proposed addition is shown in bold):

>> Certain combinations of static type of the left-hand-side and the given type are considered incompatible and result in compile-time error. A value of static type E is said to be pattern compatible with the type T if there exists an identity conversion, an implicit reference conversion, a boxing conversion, an explicit reference conversion, or an unboxing conversion from E to T<strong>, or if either E or T is an open type</strong>. It is a compile-time error if an expression of type E is not pattern compatible with the type in a type pattern that it is matched with.

オリジナルのパターンマッチングの仕様（proposed）はこちらになります。
[csharplang/proposals/patterns.md > Type Pattern](https://github.com/dotnet/csharplang/blob/master/proposals/patterns.md#type-pattern)

具体的な修正コードに興味がある方は pull-request [dotnet/roslyn#18784](https://github.com/dotnet/roslyn/pull/18784/commits/9d0ab48ea039d8c2ad9323c837e1f10a4d827417#diff-9fddc2f44a8ec31f14b12bfe48c59956L134) を参照してみて下さい。


## 回避策は無いのか？
> **Workarounds, if any**
Cast the expression being matched to object.

やや冗長な記述になりますが、[dotnet/roslyn#18784](https://github.com/dotnet/roslyn/pull/18784#issue-222514704) にあるように一旦 `object` にキャストする事で回避できます。

```csharp
// ここでは Packet にキャスト
if ((Packet)packet is KeepalivePacket keepalive) {
    // Do stuff with keepalive
}
```

この「一旦 `object` にキャスト」はジェネリックで良く見るパターンですね。

```csharp
string ToString<T>(T value) {
    if(value is string) {
        //return (string)value;    // compile error
        return (string)(object)value;
    }
    ...
}
```

これが仕様でしたら、この issue も「仕様です☆」で返して良かったと思います。


## 非ジェネリックでは駄目なのか？

実は dotnet/roslyn#16195 の例、そもそもジェネリックを使わずともベースクラスや共通インターフェースで引数を受ける事ができ、シンプルです。

```csharp:dotnet/roslyn#16195
public class Packet {}
public class KeepalivePacket : Packet {}

public void Send<T>(T packet)
    where T : Packet
{
    if (packet is KeepalivePacket keepalive)
    {
        // Do stuff with keepalive
    }

    switch (packet)
    {
        case KeepalivePacket keepalivePacket:
            // Do stuff with keepalivePacket
            break;
    }
}
```
<div>:arrow_down:</div>
```csharp:非ジェネリック版
public class Packet {}
public class KeepalivePacket : Packet {}

public void Send(Packet packet)
{
    if (packet is KeepalivePacket keepalive)
    {
        // Do stuff with keepalive
    }

    switch (packet)
    {
        case KeepalivePacket keepalivePacket:
            // Do stuff with keepalivePacket
            break;
    }
}
```

ジェネリックを使う理由の一つに boxing を回避する事が挙げられますので、その為にジェネリックを採用したいという動機もあるかも知れません。
一方で、パターンマッチングは `isinst` 命令で行われている為、比較対象のオブジェクト参照を必要とします。それゆえ、型引数に値型を渡したとしても必ず boxing が生じてしまいます。

```csharp:ジェネリックの型パラメーターを使用した例
public void Display<T>(T value) {
    switch (value) {
        case int n:
            break;
    }
}
```
<div>:arrow_down:</div>
```csharp:IL
.method public hidebysig instance void  Display<T>(!!T 'value') cil managed
{
  // コード サイズ       41 (0x29)
  .maxstack  2
  .locals init (!!T V_0,
           int32 V_1,
           object V_2)
  IL_0000:  ldarg.1
  IL_0001:  stloc.0
  IL_0002:  ldloc.0
  IL_0003:  box        !!T
  IL_0008:  brfalse.s  IL_0028
  IL_000a:  ldloc.0
  IL_000b:  box        !!T
  IL_0010:  stloc.2
  IL_0011:  ldloc.2
  IL_0012:  isinst     [System.Runtime]System.Int32
  IL_0017:  ldnull
  IL_0018:  cgt.un
  IL_001a:  dup
  IL_001b:  brtrue.s   IL_0020
  IL_001d:  ldc.i4.0
  IL_001e:  br.s       IL_0026
  IL_0020:  ldloc.2
  IL_0021:  unbox.any  [System.Runtime]System.Int32
  IL_0026:  stloc.1
  IL_0027:  pop
  IL_0028:  ret
} // end of method MyClass::Display
```

ちなみに、インターフェースを使用した例（非ジェネリック）では、引数が既に参照型のため、boxing は発生していません（`int` への変換のため unboxing は発生していますけど）。

```csharp:インターフェースを使用した例
public void Display(IFormattable value) {
    switch (value) {
        case int n:
            break;
    }
}
```
<div>:arrow_down:</div>
```csharp:IL
.method public hidebysig instance void  Display(class [System.Runtime]System.IFormattable 'value') cil managed
{
  // コード サイズ       31 (0x1f)
  .maxstack  2
  .locals init (class [System.Runtime]System.IFormattable V_0,
           int32 V_1,
           object V_2)
  IL_0000:  ldarg.1
  IL_0001:  stloc.0
  IL_0002:  ldloc.0
  IL_0003:  brfalse.s  IL_001e
  IL_0005:  ldloc.0
  IL_0006:  stloc.2
  IL_0007:  ldloc.2
  IL_0008:  isinst     [System.Runtime]System.Int32
  IL_000d:  ldnull
  IL_000e:  cgt.un
  IL_0010:  dup
  IL_0011:  brtrue.s   IL_0016
  IL_0013:  ldc.i4.0
  IL_0014:  br.s       IL_001c
  IL_0016:  ldloc.2
  IL_0017:  unbox.any  [System.Runtime]System.Int32
  IL_001c:  stloc.1
  IL_001d:  pop
  IL_001e:  ret
} // end of method MyClass::Display
```

実引数が値型の場合には、後者の非ジェネリック版の方が速かったりします（参照型の場合は同程度）。
簡単なパフォーマンス比較も行いましたので参考にしてみて下さい。
[pattern-matching におけるジェネリックと非ジェネリックのパフォーマンス比較 - GitHubGist](https://gist.github.com/in-async/358364841878ae2b53cc30012551f04f)

## ジェネリック対応は不要なのか？
上記のようにジェネリック無しで書ける事が殆どだと思いますが、例えば [dotnet/roslyn#16993](https://github.com/dotnet/roslyn/issues/16993) のようなケースでは _pattern-matching with generics_ を用いても良いかも知れません。
>
```csharp
namespace NS1
{
    public interface ITest<T> {}
>
    internal class Test1 : ITest<int> { }
    internal sealed class Test2 : ITest<int> { }
>
    internal class C<T>
    {
        public void F(ITest<T> x)
        {
            if (x is Test1 z) { }
            if (x is Test2 y) { }
        }
    }
}
```
> （中略）
**Actual Behavior**:
The `if (x is Test2 y) { }` line results in the error:
`CS8121 An expression of type ITest<T> cannot be handled by a pattern of type Test2`

`Test1` と `Test2` の違いは `sealed` 修飾子の有無だけです。これが命運を分けます。

簡単に説明しますと、`ITest<T>` と `Test1` との間には参照変換があります[^2]が、`ITest<T>` と `Test2` には変換が存在しません。その為 C# 7.0 では `if (x is Test2 y) { }` はコンパイルエラーとなってしまいます。

[^2]: 「From any _interface_type_ `S` to any _class_type_ `T`, provided `T` is not sealed or provided `T` implements `S`.」[Conversions | Microsoft Docs > Explicit reference conversions](https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/language-specification/conversions#explicit-reference-conversions)

これは判り難い！

_pattern-matching with generics_ があれば、オペランド又は _type pattern_ がオープン型であれば変換が無くても `if (x is Test2 y) { }` は期待通りに実行できます。

……でも、

`F()` の引数を `ITest<int>` にしたり、または非ジェネリックな `ITest` インターフェースを用意すれば良いと思うのですけどね。
（そもそも例にあるクラス設計について、実用的にこのようになる事ってあるのでしょうか？）

## 総括
ジェネリックでパターンマッチングを行うと、

- 型引数が値型の場合に boxing が発生し、パフォーマンスに影響が出る。
- コードの複雑度が増す（非ジェネリックの方が理解し易く、コードも短い）。

以上から、_pattern-matching with generics_ の使用は個人的にはお勧めしません。代わりに、ジェネリックを利用しない方法を考慮してみて下さい。どうしても他にスマートな解決手段が見つからない場合に仕方なく採る選択肢、という消極的な位置づけが良いと思います。

もし他に、もっと _pattern-matching with generics_ の有用なケースをご存知の方がいらっしゃいましたら、コメントでお知らせ頂けますと幸いです。


## See also

- [csharplang/generics-pattern-match.md at master · dotnet/csharplang](https://github.com/dotnet/csharplang/blob/master/proposals/csharp-7.1/generics-pattern-match.md)
- [Generic expression of a derived type cannot be handled by a pattern · Issue #16195 · dotnet/roslyn](https://github.com/dotnet/roslyn/issues/16195)
- [Is T expressions result in CS8121 error if sealed class, implementing a generic interface, is used for T · Issue #16993 · dotnet/roslyn](https://github.com/dotnet/roslyn/issues/16993)
- [Champion &quot;pattern-matching with generics&quot; (C# 7.1) · Issue #154 · dotnet/csharplang](https://github.com/dotnet/csharplang/issues/154)
- [Reference Source .NET Compiler Platform ("Roslyn") > ClassifyConversionFromType](http://source.roslyn.io/#Microsoft.CodeAnalysis.CSharp/Binder/Semantics/Conversions/ConversionsBase.cs,2d6dc8977175ff46) - 型間の `Conversion` を走査するメソッド。


## Related Posts
- [1分で分かる C# 7.1 の新機能](https://qiita.com/inasync/items/6ffa2364c3b42442c6a2)