संपादन करना

के माध्यम से साझा करें


Tutorial: Use C# to match data against patterns

This tutorial teaches you how to use pattern matching to inspect data in C#. You write small amounts of code, then you compile and run that code. The tutorial contains a series of lessons that explore different kinds of patterns supported by C#. These lessons teach you the fundamentals of the C# language.

In this tutorial, you:

  • Launch a GitHub Codespace with a C# development environment.
  • Test data for discrete values.
  • Match enum data against value.
  • Create exhaustive matches using switch expressions.
  • Match types using type patterns.

Prerequisites

You must have one of the following:

Match a value

The preceding tutorials demonstrated built-in types and types you define as tuples or records. You can check instances of these types against a pattern. Whether an instance matches a pattern determines the actions your program takes. In the examples below, you see ? after type names. This symbol allows the value of this type to be null (for example, bool? can be true, false, or null). For more information, see Nullable value types. Let's start to explore how you can use patterns.

Open a browser window to GitHub codespaces. Create a new codespace from the .NET Template. If you completed other tutorials in this series, you can open that codespace.

  1. When your codespace loads, create a new file in the tutorials folder named patterns.cs.

  2. Open your new file.

  3. All the examples in this tutorial use text input that represents a series of bank transactions as comma separated values (CSV) input. In each of the samples, you can match the record against a pattern by using either an is or switch expression. This first example splits each line on the , character and then matches the first string field against the value "DEPOSIT" or "WITHDRAWAL" by using an is expression. When it matches, the transaction amount is added or deducted from the current account balance. To see it work, add the following code to patterns.cs:

    string bankRecords = """
        DEPOSIT,   10000, Initial balance
        DEPOSIT,     500, regular deposit
        WITHDRAWAL, 1000, rent
        DEPOSIT,    2000, freelance payment
        WITHDRAWAL,  300, groceries
        DEPOSIT,     700, gift from friend
        WITHDRAWAL,  150, utility bill
        DEPOSIT,    1200, tax refund
        WITHDRAWAL,  500, car maintenance
        DEPOSIT,     400, cashback reward
        WITHDRAWAL,  250, dining out
        DEPOSIT,    3000, bonus payment
        WITHDRAWAL,  800, loan repayment
        DEPOSIT,     600, stock dividends
        WITHDRAWAL,  100, subscription fee
        DEPOSIT,    1500, side hustle income
        WITHDRAWAL,  200, fuel expenses
        DEPOSIT,     900, refund from store
        WITHDRAWAL,  350, shopping
        DEPOSIT,    2500, project milestone payment
        WITHDRAWAL,  400, entertainment
        """;
    
    double currentBalance = 0.0;
    var reader = new StringReader(bankRecords);
    
    string? line;
    while ((line = reader.ReadLine()) is not null)
    {
        if (string.IsNullOrWhiteSpace(line)) continue;
        // Split the line based on comma delimiter and trim each part
        string[] parts = line.Split(',');
    
        string? transactionType = parts[0]?.Trim();
        if (double.TryParse(parts[1].Trim(), out double amount))
        {
            // Update the balance based on transaction type
            if (transactionType?.ToUpper() is "DEPOSIT")
                currentBalance += amount;
            else if (transactionType?.ToUpper() is "WITHDRAWAL")
                currentBalance -= amount;
    
            Console.WriteLine($"{line.Trim()} => Parsed Amount: {amount}, New Balance: {currentBalance}");
        }
    }
    
  4. Then, type the following text in the terminal window:

    cd tutorials
    dotnet patterns.cs
    
  5. Examine the output. You can see that each line is processed by comparing the value of the text in the first field.

You could similarly construct the preceding sample by using the == operator to test that two string values are equal. Comparing a variable to a constant is a basic building block for pattern matching. Let's explore more of the building blocks that are part of pattern matching.

Enum matches

Another common use for pattern matching is matching on the values of an enum type. The following sample processes the input records to create a tuple where the first value is an enum value that notes a deposit or a withdrawal. The second value is the value of the transaction.

  1. Add the following code to the end of your source file. It defines the TransactionType enumeration:

    public enum TransactionType
    {
        Deposit,
        Withdrawal,
        Invalid
    }
    
  2. Add a function to parse a bank transaction into a tuple that holds the transaction type and the value of the transaction. Add the following code before your declaration of the TransactionType enum:

    static IEnumerable<(TransactionType type, double amount)> TransactionRecords(string inputText)
    {
        var reader = new StringReader(inputText);
        string? line;
        while ((line = reader.ReadLine()) is not null)
        {
            string[] parts = line.Split(',');
    
            string? transactionType = parts[0]?.Trim();
            if (double.TryParse(parts[1].Trim(), out double amount))
            {
                // Update the balance based on transaction type
                if (transactionType?.ToUpper() is "DEPOSIT")
                    yield return (TransactionType.Deposit, amount);
                else if (transactionType?.ToUpper() is "WITHDRAWAL")
                    yield return (TransactionType.Withdrawal, amount);
            }
            else {
            yield return (TransactionType.Invalid, 0.0);
            }
        }
    }
    
  3. Add a new loop to process the transaction data by using the TransactionType enumeration you declared:

    currentBalance = 0.0;
    
    foreach (var transaction in TransactionRecords(bankRecords))
    {
        if (transaction.type == TransactionType.Deposit)
            currentBalance += transaction.amount;
        else if (transaction.type == TransactionType.Withdrawal)
            currentBalance -= transaction.amount;
        Console.WriteLine($"{transaction.type} => Parsed Amount: {transaction.amount}, New Balance: {currentBalance}");
    }
    

The preceding example also uses an if statement to check the value of an enum expression. Another form of pattern matching uses a switch expression. Let's explore that syntax and how you can use it.

Exhaustive matches with switch

A series of if statements can test a series of conditions. But, the compiler can't tell if a series of if statements are exhaustive or if later if conditions are subsumed by earlier conditions. Exhaustive means that one of the if or else clauses in the series of tests handles all possible inputs. If a series of if statements are exhaustive, every possible input satisfies at least one if or else clause. Subsumption means that a later if or else clause can't be reached because earlier if or else clauses match all possible inputs. For example, in the following example code, one clause never matches:

int n = GetNumber();

if (n < 20)
    Console.WriteLine("n is less than 20");
else if (n < 10)
    Console.WriteLine("n is less than 10"); // unreachable
else
    Console.WriteLine("n is greater than 20");

The else if clause never matches because every number less than 10 is also less than 20. The switch expression ensures both of those characteristics are met, which results in fewer bugs in your apps. Let's try it and experiment.

  1. Copy the following code. Replace the two if statements in your foreach loop with the switch expression you copied.

    currentBalance += transaction switch
    {
        (TransactionType.Deposit, var amount) => amount,
        (TransactionType.Withdrawal, var amount) => -amount,
        _ => 0.0,
    };
    
  2. Type dotnet patterns.cs in the terminal window to run the new sample.

    When you run the code, you see that it works the same.

  3. To demonstrate subsumption, reorder the switch arms as shown in the following snippet:

    currentBalance += transaction switch
    {
        (TransactionType.Deposit, var amount) => amount,
        _ => 0.0,
        (TransactionType.Withdrawal, var amount) => -amount,
    };
    

    After you reorder the switch arms, type dotnet patterns.cs in the terminal window. The compiler issues an error because the arm with _ matches every value. As a result, that final arm with TransactionType.Withdrawal never runs. The compiler tells you that something's wrong in your code.

    The compiler issues a warning if the expression tested in a switch expression could contain values that don't match any switch arm. If some values could fail to match any condition, the switch expression isn't exhaustive. The compiler also issues a warning if some values of the input don't match any of the switch arms.

  4. Remove the line with _ => 0.0,, so that any invalid values don't match.

  5. Type dotnet patterns.cs to see the results.

    The compiler issues a warning. The test data is valid, so the program works. However, any invalid data would cause a failure at runtime.

Type patterns

To finish this tutorial, explore one more building block for pattern matching: the type pattern. A type pattern tests an expression at run time to see if it's the specified type. You can use a type test with either an is expression or a switch expression. Modify the current sample in two ways. First, instead of a tuple, build Deposit and Withdrawal record types that represent the transactions.

  1. Add the following declarations at the end of your code file:

    public record Deposit(double Amount, string description);
    public record Withdrawal(double Amount, string description);
    
  2. Add this method just before the declaration of the TransactionType enumeration. It parses the text and returns a series of records:

    static IEnumerable<object?> TransactionRecordType(string inputText)
    {
        var reader = new StringReader(inputText);
        string? line;
        while ((line = reader.ReadLine()) is not null)
        {
            string[] parts = line.Split(',');
    
            string? transactionType = parts[0]?.Trim();
            if (double.TryParse(parts[1].Trim(), out double amount))
            {
                // Update the balance based on transaction type
                if (transactionType?.ToUpper() is "DEPOSIT")
                    yield return new Deposit(amount, parts[2]);
                else if (transactionType?.ToUpper() is "WITHDRAWAL")
                    yield return new Withdrawal(amount, parts[2]);
            }
            yield return default;
        }
    }
    
  3. Add the following code after the last foreach loop:

    currentBalance = 0.0;
    
    foreach (var transaction in TransactionRecordType(bankRecords))
    {
        currentBalance += transaction switch
        {
            Deposit d => d.Amount,
            Withdrawal w => -w.Amount,
            _ => 0.0,
        };
        Console.WriteLine($" {transaction} => New Balance: {currentBalance}");
    }
    
  4. Type dotnet patterns.cs in the terminal window to see the results. This final version tests the input against a type.

Pattern matching provides a vocabulary to compare an expression against characteristics. Patterns can include the expression's type, values of types, property values, and combinations of them. Comparing expressions against a pattern can be clearer than multiple if comparisons. You explored some of the patterns you can use to match expressions. There are many more ways to use pattern matching in your applications. As you explore, you can learn more about pattern matching in C# in the following articles:

Cleanup resources

GitHub automatically deletes your Codespace after 30 days of inactivity. You completed all the tutorials in this series. To delete your Codespace now, open a browser window and go to your Codespaces. You should see a list of your codespaces in the window. Select the three dots (...) in the entry for the learn tutorial codespace and select delete.