はじめに
パターンマッチングの補完機能として C# 7.1 で追加された pattern-matching with generics。
始めは「型パラメーターの変数もマッチングできた方が自然だよね」と思いましたが、そもそもこのジェネリックでのパターンマッチング、どこで使うのでしょうか?
ユースケースは様々で正解はないと思いますが、個人的にはコードの複雑度を上げるだけと考えるのでお勧めしません。
この記事では機能追加のあらましとお勧めしない理由を記します。
なぜ機能は追加されたのか?
csharp-7.1/generics-pattern-match.md には次のように書かれています。
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 を読んでみましょう。
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.
var keepalive = packet as KeepalivePacket;
if (keepalive != null)
{
// Do stuff with keepalive
}
上記のコードパターンの際に、Roslyn はパターンマッチングの使用を推奨(IDE0019)するようになっていますが、実際に適用すると C# 7.0 ではコンパイルエラーになります。これが機能追加の理由のようです。
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 に下記がコメントされています。
@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 の検証時、オペランドとの間に変換が無い場合に ERR_PatternWrongType
としていました。しかしオペランドと type pattern の何れかががオープン型1の場合には変換が存在しないので、このケースに当たってしまうという訳です。
この動作はプログラムのバグではなく仕様漏れに相当するので、仕様に追記する形で修正されました。
csharplang/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, or if either E or T is an open type. 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
具体的な修正コードに興味がある方は pull-request dotnet/roslyn#18784 を参照してみて下さい。
回避策は無いのか?
Workarounds, if any
Cast the expression being matched to object.
やや冗長な記述になりますが、dotnet/roslyn#18784 にあるように一旦 object
にキャストする事で回避できます。
// ここでは Packet にキャスト
if ((Packet)packet is KeepalivePacket keepalive) {
// Do stuff with keepalive
}
この「一旦 object
にキャスト」はジェネリックで良く見るパターンですね。
string ToString<T>(T value) {
if(value is string) {
//return (string)value; // compile error
return (string)(object)value;
}
...
}
これが仕様でしたら、この issue も「仕様です☆」で返して良かったと思います。
非ジェネリックでは駄目なのか?
実は 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;
}
}
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;
}
}
ちなみに、インターフェースを使用した例(非ジェネリック)では、引数が既に参照型のため、boxing は発生していません(int
への変換のため unboxing は発生していますけど)。
public void Display(IFormattable value) {
switch (value) {
case int n:
break;
}
}
実引数が値型の場合には、後者の非ジェネリック版の方が速かったりします(参照型の場合は同程度)。
簡単なパフォーマンス比較も行いましたので参考にしてみて下さい。
pattern-matching におけるジェネリックと非ジェネリックのパフォーマンス比較 - GitHubGist
ジェネリック対応は不要なのか?
上記のようにジェネリック無しで書ける事が殆どだと思いますが、例えば dotnet/roslyn#16993 のようなケースでは pattern-matching with generics を用いても良いかも知れません。
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:
Theif (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) { }
はコンパイルエラーとなってしまいます。
これは判り難い!
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
- Generic expression of a derived type cannot be handled by a pattern · Issue #16195 · dotnet/roslyn
- Is T expressions result in CS8121 error if sealed class, implementing a generic interface, is used for T · Issue #16993 · dotnet/roslyn
- Champion "pattern-matching with generics" (C# 7.1) · Issue #154 · dotnet/csharplang
-
Reference Source .NET Compiler Platform ("Roslyn") > ClassifyConversionFromType - 型間の
Conversion
を走査するメソッド。
Related Posts
-
大雑把に言えば、型パラメーターを包含している型。e.g.
IEnumerable<T>
。オープン型以外は全てクローズ型。型引数が与えられたIEnumerable<int>
はクローズ型。Types | Microsoft Docs > Open and closed types ↩ -
「From any interface_type
S
to any class_typeT
, providedT
is not sealed or providedT
implementsS
.」Conversions | Microsoft Docs > Explicit reference conversions ↩