はじめに
C#も2021年現在ではC# 10.0
が登場し、古いC#とは比べ物にならないほど様々な機能が追加され、ますます便利になってきました。
その中でも個人的にイチオシの機能が「パターンマッチング
」です。
この機能のおかげでめちゃくちゃ実装が捗るようになったので今回はこちらを紹介します。
また、パターンマッチングと直接の関係はありませんが、「タプル(ValueTuple
)」という機能もC#にはあります。ValueTuple
は自分で定義した型に対してパターンマッチングを利用する際に使うことがあるので、ついでにこちらも紹介します。
タプル(ValueTuple)(C# 7.0~)
タプルとは「複数のデータを一時的に扱う」ときに生成されるオブジェクトです。
すごく簡単にいうと、使い捨て用途のクラス/構造体の定義を極限まで簡単にしたものです。
タプルという概念自体はC#6以前からも存在し、.NET Frameworkが提供するTupleクラスを用いれば利用自体は可能でした。
これが言語機能として組み込まれさらに気軽に扱えるようになったのがC#7.0~です。
このC#7.0~から用いることができるタプルの実装はValueTuple
構造体と呼ばれています。
(以下、「タプル」はすべてValueTuple
を指します)
タプルの使用例
タプルは、メソッドの戻り値として同時に複数の型を返したり、関連するデータ群を一時的にまとめておくなどに用います。「いくつかの変数をセットにして扱いたいが、かといってわわざわざ新しい型を定義するまでもない」といったシチュエーションにおいて便利に使えます。
/// <summary>
/// 返り値として「成否を表すbool」と「その結果」を返すメソッド
/// </summary>
private static (bool, int) ParseStringToInt(string origin)
{
var result = int.TryParse(origin, out var value);
return (result, value);
}
static void Main(string[] args)
{
// 結果を受け取ってタプルとして保存(ただしこの記法は冗長)
var tuple = ParseStringToInt("100");
if (tuple.Item1)
{
Console.Write(tuple.Item2);
}
else
{
Console.Write("Parse failed.");
}
}
記法
タプルの記法はシンプルで、()
で包んでその中にカンマ区切りでデータを入れることでタプル化することができます。
// ( ) で包めばなんでもValueTupleにできる
// この場合は(int,string,double)の3つが入ったValueTupleができる
var tuple = (10, "name", -1.0);
要素に名前をつける
タプルを定義する際、要素に名前をつけることも可能です。
// 要素に名前をつける
var tuple = (id: 10, name: "name", price: -1.0);
// つけた名前で呼び出せる
Console.WriteLine(tuple.price);
分解
タプルを別のタプルに代入する際、それぞれの要素を取り出して別々に代入することができます。
これを「タプルの分解」と呼びます。
// こういうタプルがあったときに
var tuple = (id: 10, name: "name", price: -1.0);
// 先に定義された変数があって
int x;
string y;
// それぞれの変数に合わせて分解して代入する
// 不要な要素は _ を指定することで無視できる
(x, y, _) = tuple;
Console.WriteLine(x);
また、分解して代入する際に変数定義を同時に行うことも可能です。
(分解宣言)
// こういうタプルがあったときに
var tuple = (id: 10, name: "name", price: -1.0);
// 変数定義と同時に分解して代入
(var x, var y, _) = tuple;
// 変数をまとめて宣言する場合は頭に var でもOK
var (a, b, c) = tuple;
任意の型を分解する(任意の型をタプルとして扱えるようにする)
C# 7
以降では任意の型にDeconstruct
という名前のメソッドが定義されていた場合(拡張メソッドでもOK)、これを分解時に呼び出すという機能が追加されています。
このDeconstruct
を用いることで、クラスや構造体から一部分のみを抽出してタプルとして扱うことができるようになります。
/// <summary>
/// ユーザ構造体があったとして
/// </summary>
public readonly struct User
{
public int UserId { get; }
public string UserName { get; }
public string MailAddress { get; }
public User(int userId, string userName, string mailAddress)
{
UserId = userId;
UserName = userName;
MailAddress = mailAddress;
}
/// <summary>
/// Deconstruct を定義
/// この名前のメソッドを指定した形で定義しておくことで、分解時に自動的に呼び出される
/// </summary>
public void Deconstruct(out int id, out string name) => (id, name) = (UserId, UserName);
}
// 構造体をインスタンス化
var user = new User(1, "Taro", "hoge@example.com");
// 構造体を分解
// 内部でDeconstructが呼び出されてタプルに変換された後に分解される
var (id, name) = user;
このDeconstruct
は後述するパターンマッチングと合わせて用いることができます。
分解のネスト
分解はネストしていても実行することができます。
/// <summary>
/// XとYを2つの要素を持つベクトル
/// </summary>
public readonly struct Vector2
{
public float X { get; }
public float Y { get; }
// タプルに分解する
public void Deconstruct(out float x, out float y) => (x, y) = (X, Y);
public Vector2(float x, float y)
{
X = x;
Y = y;
}
}
/// <summary>
/// Vector2を2つもつ構造体
/// </summary>
public readonly struct Vector2Pair
{
public Vector2 A { get; }
public Vector2 B { get; }
public void Deconstruct(out Vector2 a, out Vector2 b) => (a, b) = (A, B);
public Vector2Pair(Vector2 a, Vector2 b)
{
A = a;
B = b;
}
}
// Vector2を2つ保持する構造体
var pair = new Vector2Pair(new Vector2(1, 2), new Vector2(3, 4));
// PairをVector2 v1, Vector2 v2 に分解
var (v1, v2) = pair;
// Vector2の内容を同時に分解することもできる
var ((x1, y1), (x2, y2)) = pair;
パターンマッチング
さて、本題のパターンマッチングです。
パターンマッチングとはデータの型およびその中身を精査し、指定したパターンと一致した場合に処理を実行する機能のことです。
簡単にいえば、「めちゃくちゃ賢く便利になったswitch文/switch式」です。
型でパターンマッチング
型でswitchする C# 7.0~
C# 7.0
以降のバージョンであれば、switch
文を「型」で判定して動かすことが可能になります。
たとえば次のようなインタフェースとその実装があったとします。
/// <summary>
/// なにかのデータのソースを表すインタフェース
/// </summary>
public interface ISource
{
}
/// <summary>
/// ローカルパスを指すデータソース
/// </summary>
public readonly struct LocalSource : ISource
{
public string Path { get; }
public LocalSource(string path)
{
Path = path;
}
}
/// <summary>
/// ネットワーク上のパスを指すデータソース
/// </summary>
public readonly struct NetworkSource : ISource
{
public string Uri { get; }
public NetworkSource(string uri)
{
Uri = uri;
}
}
もしISource
が渡された時に、その型に応じて処理を分岐したいという場合は次のように書くことができます。(OCP違反な気もするけど他に例えが思いつかなかったのでゆるして)
public void NankaMethod(ISource source)
{
// 型に応じてswitchを分岐させることができる
switch (source)
{
// source が LocalSource型 だった
case LocalSource localSource:
Console.Write(localSource.Path);
break;
// source が NetworkSource型 だった
case NetworkSource networkSource:
Console.Write(networkSource.Uri);
break;
default:
throw new ArgumentOutOfRangeException(nameof(source));
}
}
複数オブジェクトの型の組み合わせをみてswitch C# 8.0~
C# 8.0
以降であればこの記法が少し拡張され、複数の型を同時に判定することが可能になります。
これにより複数の型の組み合わせによって動作が変わる場合の処理が書きやすくなります。
public void NankaMethod2(ISource source1, ISource source2)
{
// C# 8.0以降なら複数のデータを同時にswitchに渡してパターンマッチングできる
switch (source1, source2)
{
// source1 と source2 が両方とも NetworkSource型 だったとき
case (NetworkSource n1, NetworkSource n2):
Console.WriteLine($"{n1.Uri}/{n2.Uri}");
break;
// source1 が NetworkSource型で、source2 が LocalSource型 の組み合わせだったとき
case (NetworkSource n1, LocalSource l1):
Console.WriteLine($"{n1.Uri}/{l1.Path}");
break;
// _ と var はそれぞれワイルドカード扱い
// _ の場合はマッチした要素は破棄される(代入せず無視する)
// var の場合は型によらず必ず代入される
// source1 が LocalSource型だったとき(source2の型はなんでもよい)
case (LocalSource _, var s2):
break;
}
}
型と定数で比較
型とその内容をみて判定 C# 7.0~
パターンマッチング時、定数を指定してさらに処理を細かく分岐することもできます。
case
の後に続けてwhen
で条件式を記述することで、細かく条件分岐が可能になります。
public string NankaMethod3(object obj)
{
// Object型をそれぞれの型で判定し、その中身も同時に判定する
switch (obj)
{
// objがint型 かつ 0より大きい
case int x when x > 0:
return x.ToString();
// objがint型 かつ 0以下
case int x when x <= 0:
return (-x).ToString();
// objがfloat型
case float f:
return ((int) f).ToString();
// objが1文字以上のstring型
case string s when s.Length > 0:
return s;
// どれにもマッチしなかった
default:
throw new ArgumentOutOfRangeException(nameof(obj));
}
}
switch式で簡略化 C# 8.0~
C# 8.0以降であればswitch式
という記法が使えます。
(switch文
を簡略化したもの)
これによりさきほどのパターンマッチングの記法は次のように書き換えることもできます。
public string NankaMethod3(object obj)
{
// Object型をそれぞれの型で判定し、その中身も同時に判定する
return obj switch
{
// objがint型 かつ 0より大きい
int x when x > 0 => x.ToString(),
// objがint型 かつ 0未満
int x when x <= 0 => (-x).ToString(),
// objがfloat型
float f => ((int) f).ToString(),
// objが1文字以上のstring型
string s when s.Length > 0 => s,
// どれにもマッチしなかった
_ => throw new ArgumentOutOfRangeException(nameof(obj))
};
}
さらに簡略化 C# 9.0~
C# 9.0
以降であれば、パターンマッチング時の定数との比較をさらに簡略化して書くことができます。
(when
が不要になる)
public string NankaMethod3(object obj)
{
// Object型をそれぞれの型で判定し、その中身も同時に判定する
return obj switch
{
// objがint型 かつ 0より大きい
int x and > 0 => x.ToString(),
// objがint型 かつ 0未満
int x and <= 0 => (-x).ToString(),
// objがfloat型
float f => ((int) f).ToString(),
// objが1文字以上のstring型
string {Length: > 0} s => s,
_ => throw new ArgumentOutOfRangeException(nameof(obj))
};
}
Deconstructと併用する
任意の型でパターンマッチング C# 8.0~
さきほどの構造体やクラスをタプルに分解するDeconstruct
ですが、パターンマッチングと併用することができます。
/// <summary>
/// XとYを2つの要素を持つベクトル
/// </summary>
public readonly struct Vector2
{
public float X { get; }
public float Y { get; }
// タプルに分解する
public void Deconstruct(out float x, out float y) => (x, y) = (X, Y);
public Vector2(float x, float y)
{
X = x;
Y = y;
}
}
このVector2
型はswitch文
に渡すことで自動的にタプルに分解され、タプルの要素ごとのパターンマッチングが使えるようになります。
public void NankaMethod4(Vector2 vector2)
{
// vector2が自動的に(float x, float y)のタプルに分解され、
// このタプルに対するパターンマッチングが実行される
switch (vector2)
{
// xがゼロ
case var (x, _) when x == 0:
Console.Write("0,?");
break;
// yがゼロ
case var (_, y) when y == 0:
Console.Write("?,0");
break;
// xとyの正負の組み合わせ
case var (x, y) when x > 0 && y > 0:
Console.Write("+,+");
break;
case var (x, y) when x < 0 && y > 0:
Console.Write("-,+");
break;
case var (x, y) when x > 0 && y < 0:
Console.Write("+,-");
break;
case var (x, y) when x < 0 && y < 0:
Console.Write("-,-");
break;
}
}
構造体の中身に応じて複雑に条件分岐したい場合などはこのDeconstruct
とパターンマッチングを併用するとよいでしょう。またDeconstruct
は拡張メソッドとして定義されていてもOKです。
プロパティを指定してパターンマッチング
構造体のプロパティを取り出して判定 C# 8.0~
C# 8.0
以降であれば、対象のオブジェクトのプロパティを条件式で指定してパターンマッチングすることができます。
(そのためプロパティのみを用いて比較するのであればわざわざDeconstruct
を定義しなくても済みます)
public readonly struct Vector3
{
public float X { get; }
public float Y { get; }
public float Z { get; }
// Deconstructは定義してない
public Vector3(float x, float y, float z)
{
X = x;
Y = y;
Z = z;
}
}
public void NankaMethod6(Vector3 vector3)
{
// Vector3のプロパティを取り出して判定
switch (vector3)
{
// Zが0、XとYは考慮外
case {Z: 0}:
Console.Write("?,?,0");
break;
// Zが0未満、XとYは考慮外
case {Z: var z} when z < 0:
Console.Write("?,?,-");
break;
// XとYが正、Zは正
case {X: var x, Y: var y} when x > 0 && y > 0:
Console.Write("+,+,+");
break;
// その他の組み合わせパターン
default:
Console.Write("Others");
break;
}
}
簡略化 C# 9.0~
C# 8.0
以降であれば、プロパティを指定する部分に直接条件を書く省略記法も使えます。
public static void NankaMethod7(Vector3 vector3)
{
// Vector2のXとYをそれぞれ指定して判定
switch (vector3)
{
// Zが0、XとYは考慮外
case {Z: 0}:
Console.Write("?,?,0");
break;
// Zが0未満、XとYは考慮外
case {Z: < 0}:
Console.Write("?,?,-");
break;
// XとYが正、Zは正
case {X: > 0, Y: > 0}:
Console.Write("+,+,+");
break;
// その他の組み合わせパターン
default:
Console.Write("Others");
break;
}
}
まとめ
C#
はバージョン7,8,9と続いてパターンマッチング周りが大幅に強化されました。これにより込み入った処理を比較簡単に記述できるようになり、もうif文
やis
でがんばって判定する必要はなくなりました。
ぜひこの新しい構文に慣れて使いこなせるようになりましょう。
(なおパターンマッチングが周りはまだまだ機能が多く、実はすべて紹介しきれていません)