はじめに
C#での条件分岐といえば、if-else
やswitch
文を使うのが一般的です。しかし、条件が複雑になってくると、コードは徐々に読みづらくなっていきます。特にオブジェクトの型チェックや、プロパティの条件分岐が組み合わさると、入れ子になったif
文の波カッコの嵐に見舞われることも...
そんな悩みを解決してくれるのがパターンマッチングです。
パターンマッチングとは
パターンマッチングは、データの構造やプロパティを検査して、マッチするパターンに応じた処理を行う機能です。簡単に言えば、「このデータはこういう形をしているはず」というパターンを定義し、そのパターンに一致した場合の処理を書くことができます。
従来の条件分岐と比べて以下のような利点があります。
- 複雑な条件をシンプルに表現できる
- コードの意図が分かりやすい
- 条件の漏れを防ぎやすい
- 型の安全性が保証される
使用条件
1. 開発環境の要件
- C# 7.0以降:基本的なパターンマッチング
- C# 8.0以降:switch式とプロパティパターン
- C# 9.0以降:関係パターン(>、<、>=、<=)
- C# 10.0以降:拡張プロパティパターン
2. フレームワークの要件
- .NET Core 3.0以降
- .NET 5以降
- ※.NET Framework(4.8以前)では最新の機能は使用できません
3. 開発ツール
- Visual Studio 2019(バージョン16.3以降)
- Visual Studio 2022
- その他.NET Core/.NETをサポートする開発環境
4. プロジェクトの設定
- プロジェクトファイル(.csproj)で適切な言語バージョンの設定が必要な場合があります:
<PropertyGroup>
<LangVersion>latest</LangVersion>
</PropertyGroup>
C#でのパターンマッチングの進化
C#では、バージョン7.0から本格的にパターンマッチングのサポートが始まりました。その後のバージョンアップで機能が大幅に強化され、現在では非常に強力な機能となっています。
C#のパターンマッチング履歴
- C# 7.0:
is
パターンとswitch
式での基本的なパターンマッチング - C# 8.0:
switch
式の導入、プロパティパターンの追加 - C# 9.0: 関係パターン(
>
、<
、>=
、<=
)の追加 - C# 10.0: 拡張プロパティパターン、パターンの改善
特に新しいバージョンで追加された機能により、従来は複数行のコードで書かなければならなかった条件分岐が、1行で簡潔に書けるようになりました。
それでは、具体的なコード例を見ながら、パターンマッチングの実践的な使い方を見ていきましょう。
シンプルな型のパターンマッチング
// 従来の書き方(if-else + is)
public string DescribeValue(object value)
{
if (value is int i)
return $"整数値: {i}";
else if (value is string s)
return $"文字列: {s}";
return "その他の型";
}
// パターンマッチングを使用
public string DescribeValue(object value) => value switch
{
int i => $"整数値: {i}",
string s => $"文字列: {s}",
_ => "その他の型"
}
プロパティパターンの活用
// 従来の書き方
public string GetUserStatus(User user)
{
if (user.Age < 20)
return "未成年";
if (user.Premium && user.LoginCount > 100)
return "プレミアムヘビーユーザー";
if (user.LoginCount > 100)
return "ヘビーユーザー";
return "通常ユーザー";
}
// パターンマッチング
public string GetUserStatus(User user) => user switch
{
{ Age: < 20 } => "未成年",
{ Premium: true, LoginCount: > 100 } => "プレミアムヘビーユーザー",
{ LoginCount: > 100 } => "ヘビーユーザー",
_ => "通常ユーザー"
}
タプルパターンの活用例
// 従来の書き方
public string GetDirection(Point start, Point end)
{
int dx = end.X - start.X;
int dy = end.Y - start.Y;
if (dx > 0 && dy == 0) return "右";
if (dx < 0 && dy == 0) return "左";
if (dx == 0 && dy > 0) return "上";
if (dx == 0 && dy < 0) return "下";
return "斜め";
}
// パターンマッチング
public string GetDirection(Point start, Point end) => (end.X - start.X, end.Y - start.Y) switch
{
( > 0, 0) => "右",
( < 0, 0) => "左",
(0, > 0) => "上",
(0, < 0) => "下",
_ => "斜め"
}
パターンの組み合わせ
パターンマッチングの真価は、複数のパターンを組み合わせることで発揮されます。実践的な例を見ながら、高度な使用方法を紹介していきます。
複数パターンの組み合わせ
まず、オンラインショップの注文処理を例に見てみましょう。
public record Order(decimal Amount, string Status, bool IsPriority, Address ShippingAddress);
public record Address(string Country, string Prefecture);
// 従来の書き方
public string GetShippingMethod(Order order)
{
if (order.Status == "Cancelled")
return "配送なし";
if (order.ShippingAddress.Country != "Japan")
{
if (order.Amount >= 100000)
return "国際優先配送";
return "国際通常配送";
}
if (order.IsPriority && order.Amount >= 50000)
return "国内翌日配送";
if (order.ShippingAddress.Prefecture == "沖縄" ||
order.ShippingAddress.Prefecture == "北海道")
return "地域特別配送";
return "国内通常配送";
}
// パターンマッチングを使用した書き方
public string GetShippingMethod(Order order) => order switch
{
{ Status: "Cancelled" } => "配送なし",
{ ShippingAddress: { Country: not "Japan" }, Amount: >= 100000 } => "国際優先配送",
{ ShippingAddress: { Country: not "Japan" } } => "国際通常配送",
{ IsPriority: true, Amount: >= 50000 } => "国内翌日配送",
{ ShippingAddress: { Prefecture: "沖縄" or "北海道" } } => "地域特別配送",
_ => "国内通常配送"
}
whenガードとの組み合わせ
より複雑な条件を扱う場合は、when
ガードを使用できます。天気予報アプリの通知メッセージを例に見てみましょう。
public record WeatherForecast(
decimal Temperature,
decimal Humidity,
decimal RainProbability,
decimal WindSpeed,
DateTime ForecastTime);
// 従来の書き方
public string GetWeatherAlert(WeatherForecast forecast)
{
var hour = forecast.ForecastTime.Hour;
if (forecast.RainProbability > 80 && forecast.WindSpeed > 15)
{
if (hour >= 6 && hour <= 22)
return "強い雨と風が予想されます。外出時はご注意ください。";
return "強い雨と風が予想されます。";
}
if (forecast.Temperature >= 35 && forecast.Humidity > 70)
return "危険な暑さです。熱中症にご注意ください。";
if (forecast.Temperature <= 0 && hour >= 6 && hour <= 9)
return "朝の凍結にご注意ください。";
return "特に警報はありません。";
}
// パターンマッチングを使用した書き方
public string GetWeatherAlert(WeatherForecast forecast) => forecast switch
{
{ RainProbability: > 80, WindSpeed: > 15 }
when forecast.ForecastTime.Hour is >= 6 and <= 22
=> "強い雨と風が予想されます。外出時はご注意ください。",
{ RainProbability: > 80, WindSpeed: > 15 }
=> "強い雨と風が予想されます。",
{ Temperature: >= 35, Humidity: > 70 }
=> "危険な暑さです。熱中症にご注意ください。",
{ Temperature: <= 0 }
when forecast.ForecastTime.Hour is >= 6 and <= 9
=> "朝の凍結にご注意ください。",
_ => "特に警報はありません。"
}
このように、パターンマッチングを使うことで・・
- ネストした条件分岐が平坦化される
- 各条件のまとまりが視覚的に分かりやすくなる
- 条件の追加や変更が容易になる
特にwhen
ガードは、パターンマッチングだけでは表現しきれない複雑な条件を追加する際に便利です。
ただし、when
ガードの中に複雑な式を入れすぎると可読性が低下するので、必要最小限の使用に留めることをお勧めします。
パフォーマンスとベストプラクティス
パターンマッチングは便利な機能ですが、効果的に使うためにはいくつかの注意点があります。パフォーマンスの観点と実装のベストプラクティスについて見ていきましょう。
パフォーマンスについて
パターンマッチングのパフォーマンスは、通常のif-else
文やswitch
文と比べてほとんど違いがありません。コンパイラが最適化を行うため、実行時のオーバーヘッドは最小限に抑えられます。
ただし、以下の点には注意が必要です。
// 非効率な例
public string GetDiscountType(Customer customer) => customer switch
{
{ Orders: var o } when o.Count() > 100 => "VIP",
{ Orders: var o } when o.Count() > 50 => "Gold",
{ Orders: var o } when o.Count() > 10 => "Silver",
_ => "Regular"
}
// 効率的な例
public string GetDiscountType(Customer customer)
{
var orderCount = customer.Orders.Count(); // 一度だけ計算
return orderCount switch
{
> 100 => "VIP",
> 50 => "Gold",
> 10 => "Silver",
_ => "Regular"
};
}
ベストプラクティス
1. パターンの順序を意識する
// 良くない例:具体的なパターンが後ろにある
public string ValidateAge(int age) => age switch
{
< 0 => "無効な年齢です",
>= 0 => "有効な年齢です", // ここでマッチしてしまう
> 120 => "年齢が大きすぎます" // 到達されない
};
// 良い例:具体的なパターンを先に書く
public string ValidateAge(int age) => age switch
{
> 120 => "年齢が大きすぎます",
< 0 => "無効な年齢です",
_ => "有効な年齢です"
};
2. whenガードは最小限に
// 複雑すぎる例
public string GetUserType(User user) => user switch
{
{ } when user.Age > 20 && user.Premium &&
user.LastLogin > DateTime.Now.AddDays(-7) &&
user.Orders.Count() > 10 => "アクティブな優良顧客",
_ => "通常ユーザー"
};
// 整理された例
public string GetUserType(User user)
{
bool isActive = user.LastLogin > DateTime.Now.AddDays(-7);
bool isValuable = user.Orders.Count() > 10;
return user switch
{
{ Age: > 20, Premium: true } when isActive && isValuable
=> "アクティブな優良顧客",
_ => "通常ユーザー"
};
}
3. null考慮を忘れない
// nullチェックを含めたパターンマッチング
public string FormatAddress(Address? address) => address switch
{
null => "住所が未設定です",
{ Country: "Japan" } => $"国内住所: {address.Prefecture}",
_ => $"海外住所: {address.Country}"
};
4.デシジョンテーブルのような使い方を活用
public record ShippingRule(bool Domestic, bool Express, bool Heavy);
public decimal CalculateShipping(ShippingRule rule) => (rule.Domestic, rule.Express, rule.Heavy) switch
{
(true, false, false) => 500m, // 国内・通常・軽量
(true, true, false) => 1000m, // 国内・急送・軽量
(true, false, true) => 1500m, // 国内・通常・重量
(true, true, true) => 2000m, // 国内・急送・重量
(false, _, _) => 3000m, // 海外はすべて固定料金
};
これらの注意点を意識することで、パターンマッチングの利点を最大限に活かしながら、保守性の高いコードを書くことができます。
特に複雑な条件分岐を扱う際は、可読性とパフォーマンスのバランスを考慮しながら実装することが重要です。
まとめ
パターンマッチングは、C#での条件分岐を劇的に改善できる強力な機能です。この記事で見てきた内容を整理してみましょう。
パターンマッチングを使うべきケース
✅ 特に効果を発揮するシーンは
1. 複数のプロパティを組み合わせた条件分岐
// 従来の入れ子になりがちな条件分岐が1行で書ける
return user switch
{
{ Premium: true, LoginCount: > 100 } => "プレミアムユーザー",
_ => "通常ユーザー"
};
2. データの形に基づく分岐処理
// オブジェクトの構造に基づく分岐が直感的に書ける
return response switch
{
{ Status: 200, Data: { } data } => ProcessData(data),
{ Status: 404 } => "Not Found",
{ Error: var e } => HandleError(e),
_ => "Unknown Response"
};
3. 決定表のような条件の組み合わせ
// 条件の組み合わせが見やすく整理できる
return (userType, isVerified, hasPurchases) switch
{
(UserType.Business, true, _) => "認証済み法人",
(UserType.Personal, true, true) => "認証済み個人(購入実績あり)",
(_, _, _) => "その他"
};
コードの可読性向上のポイント
1. シンプルに保つ
- 1つのパターンマッチングに詰め込みすぎない
- 複雑な条件は変数や関数に分離する
2. パターンの順序を工夫する
- 具体的な条件を先に書く
- 汎用的な条件は後ろに配置する
- デフォルトケース(
_
)は最後に
3. 命名と構造化
- パターンマッチングを使用するメソッドには、目的を明確に示す名前をつける
- 大きな条件分岐は、小さな関数に分割することを検討する
最後に
パターンマッチングは、単なる構文の簡略化ではありません。
適切に使用することで・・
- コードの意図が明確になる
- バグの混入を防ぎやすくなる
- メンテナンス性が向上する
- チームでのコードレビューが容易になる
ただし、「できるから使う」のではなく、「読みやすくなるから使う」という姿勢が重要です。パターンマッチングは、私たちの強力な道具の一つとして、適材適所で活用していきましょう。