22 5月 2025

C#におけるパターンマッチング

現代のC#は、条件ロジックの扱い方において静かな革命を遂げました。型チェックや値の比較に冗長なif-elseの連鎖や扱いにくいswitchステートメントが必要だった時代は終わりました。特にC# 8.0以降に導入された洗練されたパターンマッチング機能は、開発者が制御フローを記述する方法を根本的に変え、コードをより表現豊かで、より簡潔に、そしてより安全にしました。

C#パターンマッチングによる制御フローの強化

C#のパターンマッチングは、式をテストし、その式が特定のパターンに一致した場合にアクションを実行するための簡潔な構文を提供します。これにより、開発者はその型、値、プロパティ、あるいは複雑なオブジェクトの構造を含むさまざまな基準に対して値をテストできます。この機能は主にis式とswitch式(またはswitchステートメント)を介して公開されます。基本的な型チェックは以前から存在していましたが、C# 8.0ではより豊富なパターンの語彙が導入され、その有用性と日常のコーディングプラクティスへの影響が大幅に拡大しました。この強化は、効果的なC#のswitchパターンマッチングとC#のifパターンマッチングにとって非常に重要であり、冗長な条件ロジックをコンパクトで理解しやすい構成に変換します。

利用可能な主要なパターンタイプと、それらが一般的なプログラミングタスクをどのように簡素化するかを見ていきましょう。それらの利点を示す明確なC#パターンマッチングの例も提供します。

C#パターンマッチングの実践的な例

パターンマッチングの真の力は、従来のPアプローチと比較したときに明らかになります。開発者はコードの可読性と簡潔さにおいて大きな改善を得られます。

宣言パターン

宣言パターンは式の実行時型をチェックし、一致した場合、その結果を新しい変数に割り当てます。これにより、多くの場合、明示的なキャストやnullチェックが不要になります。

Before:

public void ProcessAssetOld(object asset)
{
    if (asset is SoftwareLicense)
    {
        SoftwareLicense license = (SoftwareLicense)asset;
        Console.WriteLine($"Software License: {license.ProductName}, Expiration: {license.ExpirationDate.ToShortDateString()}");
    }
    else if (asset is HardwareComponent)
    {
        HardwareComponent hardware = (HardwareComponent)asset;
        Console.WriteLine($"Hardware Component: {hardware.ComponentName}, Serial: {hardware.SerialNumber}");
    }
    else
    {
        Console.WriteLine("Unknown asset type.");
    }
}

public class SoftwareLicense { public string ProductName { get; set; } public DateTime ExpirationDate { get; set; } }
public class HardwareComponent { public string ComponentName { get; set; } public string SerialNumber { get; set; } }

After (宣言パターンを使用):

public void ProcessAssetNew(object asset)
{
    if (asset is SoftwareLicense license)
    {
        Console.WriteLine($"Software License: {license.ProductName}, Expiration: {license.ExpirationDate.ToShortDateString()}");
    }
    else if (asset is HardwareComponent hardware)
    {
        Console.WriteLine($"Hardware Component: {hardware.ComponentName}, Serial: {hardware.SerialNumber}");
    }
    else
    {
        Console.WriteLine("Unknown asset type.");
    }
}

license変数とhardware変数は、パターンが一致した場合にのみスコープ内となり、割り当てられます。これにより、型の安全性が向上し、潜在的な実行時エラーが減少します。

型パターン

宣言パターンと同様に、型パターンは式の実行時型をチェックします。これは、パターン自体に新しい変数宣言が厳密に必要ないswitch式内でよく使用されます。

Before:

public decimal CalculateShippingCostOld(Shipment shipment)
{
    if (shipment == null)
        throw new ArgumentNullException(nameof(shipment));
    if (shipment.GetType() == typeof(StandardShipment))
        return 5.00m;
    if (shipment.GetType() == typeof(ExpressShipment))
        return 15.00m;
    if (shipment.GetType() == typeof(InternationalShipment))
        return 25.00m;
    return 0.00m; // Default for unknown types
}

public abstract class Shipment { }
public class StandardShipment : Shipment { }
public class ExpressShipment : Shipment { }
public class InternationalShipment : Shipment { }

After (switch式で型パターンを使用):

public decimal CalculateShippingCostNew(Shipment shipment) => shipment switch
{
    StandardShipment => 5.00m,
    ExpressShipment => 15.00m,
    InternationalShipment => 25.00m,
    null => throw new ArgumentNullException(nameof(shipment)),
    _ => 0.00m // Handle unknown shipment types
};

これは、異なる型に対する簡潔なC# switch caseパターンマッチングを示しています。

定数パターン

定数パターンは、式の結果が指定された定数値と等しいかどうかをテストします。これにより、離散値の比較が簡素化されます。

Before:

public string GetOrderStateOld(OrderStatus status)
{
    if (status == OrderStatus.Pending)
        return "Order is awaiting processing.";
    else if (status == OrderStatus.Shipped)
        return "Order has been dispatched.";
    else if (status == OrderStatus.Delivered)
        return "Order has been delivered.";
    else if (status == OrderStatus.Cancelled)
        return "Order has been cancelled.";
    else
        return "Unknown order state.";
}

public enum OrderStatus { Pending, Shipped, Delivered, Cancelled, Returned }

After (定数パターンを使用):

public string GetOrderStateNew(OrderStatus status) => status switch
{
    OrderStatus.Pending => "Order is awaiting processing.",
    OrderStatus.Shipped => "Order has been dispatched.",
    OrderStatus.Delivered => "Order has been delivered.",
    OrderStatus.Cancelled => "Order has been cancelled.",
    _ => "Unknown order state."
};

これは、特定のC#文字列パターンマッチングまたは列挙値を処理するクリーンな方法です。

リレーショナルパターン

リレーショナルパターンは、比較演算子(<><=>=)を使用して式の結果を定数と比較します。これにより、範囲チェックがはるかに読みやすくなります。

Before:

public string GetEmployeePerformanceLevelOld(int score)
{
    if (score < 50)
        return "Needs Improvement";
    else if (score >= 50 && score < 70)
        return "Meets Expectations";
    else if (score >= 70 && score < 90)
        return "Exceeds Expectations";
    else if (score >= 90)
        return "Outstanding";
    else
        return "Invalid Score"; // Should not happen with int, but for completeness
}

After (リレーショナルパターンを使用):

public string GetEmployeePerformanceLevelNew(int score) => score switch
{
    < 50 => "Needs Improvement",
    >= 50 and < 70 => "Meets Expectations",
    >= 70 and < 90 => "Exceeds Expectations",
    >= 90 => "Outstanding",
    _ => "Invalid Score" // Catches any unexpected integer values
};

これは、リレーショナルパターンと論理パターンを組み合わせてC#のパターンマッチングを行う強力な例を示しています。

プロパティパターン

プロパティパターンは、式のプロパティまたはフィールドをネストされたパターンと照合することを可能にします。これはオブジェクトの状態を検査するのに非常に便利です。

Before:

public decimal CalculateOrderDiscountOld(CustomerOrder order)
{
    if (order == null) return 0m;
    if (order.TotalAmount >= 500 && order.Customer.IsPremium)
    {
        return 0.15m; // 15% for premium customers with large orders
    }
    else if (order.TotalAmount >= 100)
    {
        return 0.05m; // 5% for general large orders
    }
    return 0m;
}

public class Customer { public bool IsPremium { get; set; } }
public class CustomerOrder { public decimal TotalAmount { get; set; } public Customer Customer { get; set; } }

After (プロパティパターンを使用):

public decimal CalculateOrderDiscountNew(CustomerOrder order) => order switch
{
    { TotalAmount: >= 500, Customer.IsPremium: true } => 0.15m,
    { TotalAmount: >= 100 } => 0.05m,
    null => 0m,
    _ => 0m
};

これは、強力なC#プロパティパターンマッチングを示し、オブジェクトの状態チェックを簡素化します。

ポジショナルパターン

ポジショナルパターンは、式の結果(タプルやDeconstructメソッドを持つレコードなど)を分解し、その結果の値をネストされたパターンと照合します。

Before:

public string DescribeTransactionOld(Transaction transaction)
{
    if (transaction.Type == TransactionType.Deposit && transaction.Amount > 1000)
        return "Large Deposit";
    else if (transaction.Type == TransactionType.Withdrawal && transaction.Amount > 500)
        return "Significant Withdrawal";
    else if (transaction.Type == TransactionType.Fee && transaction.Amount > 0)
        return "Applied Fee";
    else
        return "Standard Transaction";
}

public record Transaction(TransactionType Type, decimal Amount);
public enum TransactionType { Deposit, Withdrawal, Fee, Transfer }

After (ポジショナルパターンを使用):

public string DescribeTransactionNew(Transaction transaction) => transaction switch
{
    (TransactionType.Deposit, > 1000m) => "Large Deposit",
    (TransactionType.Withdrawal, > 500m) => "Significant Withdrawal",
    (TransactionType.Fee, > 0m) => "Applied Fee",
    _ => "Standard Transaction"
};

この簡潔な形式は、分解可能な型に対するC#のパターンマッチングの可読性を高めます。

論理パターン (andornot)

論理パターンは、andornotキーワードを使用して他のパターンを組み合わせることで、単一のパターン内で複雑な条件チェックを可能にします。

Before:

public bool CanGrantAccessOld(UserRole role, int securityLevel, bool hasTwoFactorAuth)
{
    if ((role == UserRole.Administrator || role == UserRole.Manager) && securityLevel > 7 && hasTwoFactorAuth)
        return true;
    else if (role == UserRole.Developer && securityLevel > 5)
        return true;
    else if (role != UserRole.Guest && securityLevel >= 3 && securityLevel <= 10)
        return true;
    else
        return false;
}

public enum UserRole { Guest, User, Developer, Manager, Administrator }

After (論理パターンを使用):

public bool CanGrantAccessNew(UserRole role, int securityLevel, bool hasTwoFactorAuth) => (role, securityLevel, hasTwoFactorAuth) switch
{
    (UserRole.Administrator or UserRole.Manager, > 7, true) => true,
    (UserRole.Developer, > 5, _) => true,
    (not UserRole.Guest, >= 3 and <= 10, _) => true,
    _ => false
};

C#のisパターンマッチングと論理演算子を組み合わせることで、非常に表現豊かになります。

Varパターン

varパターンは常に式と一致し、その結果を新しいローカル変数に割り当てます。これは、switch式全体の入力値(または複雑なパターンの一部)をキャプチャし、特にメソッド呼び出しや他のパターンで直接表現できないプロパティを含む、when句内で追加のより複雑なチェックを実行する必要がある場合に特に役立ちます。

Before:

using System.Collections.Generic;
using System.Linq;

public string AnalyzeNumberSequenceOld(IEnumerable<int> numbers)
{
    if (numbers == null)
        return "Invalid sequence (null)";
    if (numbers.Count() == 0)
        return "Empty sequence";
    if (numbers.Count() > 5 && numbers.Average() > 10.0)
        return "Long sequence with high average";
    return "Normal sequence";
}

After (when句でVarパターンを使用):

using System.Collections.Generic;
using System.Linq;

public string AnalyzeNumberSequenceNew(IEnumerable<int> numbers) => numbers switch
{
    null => "Invalid sequence (null)", 
    var list when list.Count() == 0 => "Empty sequence",
    var list when list.Count() > 5 && list.Average() > 10.0 => "Long sequence with high average",
    _ => "Normal sequence"
};

ここでは、var listIEnumerable<int>インスタンスをキャプチャします。これにより、when句内でlist変数に対して直接、後続の複雑なLINQクエリ(Count()Average()など)を実行でき、isを使った明示的な型チェックが不要になります。

リストパターン (C# 11)

C# 11で導入されたリストパターンは、シーケンス(配列やList<T>など)の要素と照合することを可能にします。これらはコレクションの構造と内容を検証するのに非常に強力です。

Before:

public bool ValidateLogEntryOld(string[] logParts)
{
    if (logParts == null || logParts.Length < 2) return false;

    if (logParts[0] == "ERROR" && logParts.Length >= 3 && logParts[1] == "AUTH" && logParts[2] == "FAILED")
        return true;
    if (logParts[0] == "INFO" && logParts.Length >= 2 && logParts[1] == "CONNECTED")
        return true;
    if (logParts[0] == "WARN" && logParts.Length >= 4 && logParts[1] == "DISK" && logParts[2] == "SPACE" && int.TryParse(logParts[3], out int space) && space < 10)
        return true;
    return false;
}

After (リストパターンを使用):

public bool ValidateLogEntryNew(string[] logParts) => logParts switch
{
    ["ERROR", "AUTH", "FAILED", ..] => true, // エラー認証失敗
    ["INFO", "CONNECTED", ..] => true,     // クライアント接続済み
    ["WARN", "DISK", "SPACE", var size, ..] when int.TryParse(size, out int space) && space < 10 => true, // ディスク容量不足
    _ => false // その他のログエントリ
};

これは、シーケンスに対するパターンマッチングの強力な機能であり、特にコマンドライン引数や構造が異なるログエントリを解析するシナリオで役立ちます。..(スライスパターン)は0個以上の要素と一致し、柔軟性を提供します。

基本を超えて:パターンマッチングでよりクリーンなコードを

パターンマッチングの利点は、単なる構文糖衣に留まりません。これらの機能を採用することで、開発者は本質的に以下のコードを記述できます。

  • より表現豊か: パターンは意図を直接伝え、コードを一目で読みやすく、理解しやすくします。複雑な条件ロジックが直感的になります。
  • より簡潔: ボイラープレートのif-elseブロックや明示的なキャストを排除することで、コードの量​​が減少し、保守するコードが減り、エラーの機会も減少します。
  • より安全: 特にswitch式は、網羅的なパターンマッチングを要求することがよくあります。C#コンパイラは、switch式がすべての入力値をカバーしていない場合に開発者に警告を出すことで、実行時例外の防止を支援します。このコンパイラによる網羅性の強制は、ソフトウェアの正確性を向上させます。
  • リファクタリングが容易: よりクリーンでモジュール化された条件ロジックにより、リファクタリングはそれほど困難ではなくなります。開発者はシステム​​の他の部分への影響を最小限に抑えながら、動作を変更または拡張できます。

パターンマッチングは、C#を複雑なデータ処理と制御フローにエレガントなソリューションを提供する言語に変えました。基本的な型チェックからオブジェクトの分解、シーケンスの検証まで、C# 8.0以降で提供される機能は、現代のソフトウェア開発者にとって不可欠なツールです。これらの強力な機能を採用することは、コード品質を向上させるだけでなく、開発プロセスを合理化し、より信頼性が高く保守しやすいアプリケーションに貢献します。古いスタイルのif-elseまたはswitchステートメントに依存している開発者は、これらのパターンを組み込むことで大きな恩恵を受け、C#プログラムをより堅牢で作業しやすいものにすることができます。

関連記事