-
Notifications
You must be signed in to change notification settings - Fork 6k
Add introductory interactive pattern matching tutorial #45954
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
bd68989
First pass at the code.
BillWagner d4d50a0
update code for try.net restrictions
BillWagner 3297cd9
Apply suggestions from code review
BillWagner 4e32e14
fix warnings
BillWagner ac7c696
Move some code.
BillWagner 48b36d5
make a clone
BillWagner 35337be
Finish the switch example.
BillWagner d413d8a
finish samples
BillWagner 2b72407
fix warnings
BillWagner d997a4b
typo
BillWagner d1a0a51
Finish article draft.
BillWagner c98f248
proofread
BillWagner 8a67220
fix warnings
BillWagner 7d9b3c0
Apply suggestions from code review
BillWagner 9b4e0c1
add new tutorial to TOC
BillWagner File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
--- | ||
title: Pattern matching | ||
description: In this tutorial about pattern matching, you use your browser to learn C# interactively. You're going to write C# code and see the results of compiling and running your code directly in the browser. | ||
ms.date: 05/02/2025 | ||
--- | ||
# 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 types in C#. These lessons teach you the fundamentals of the C# language. | ||
|
||
> [!TIP] | ||
> When a code snippet block includes the "Run" button, that button opens the interactive window, or replaces the existing code in the interactive window. When the snippet doesn't include a "Run" button, you can copy the code and add it to the current interactive window. | ||
|
||
The preceding tutorials demonstrated built-in types and types you define as tuples or records. Instances of these types can be checked against a *pattern*. Whether an instance matches a pattern determines the actions your program takes. Let's start to explore how you can use patterns. | ||
|
||
## Match a value | ||
|
||
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 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" using an `is` expression. When it matches, the transaction amount is added or deducted from the current account balance. To see it work, press the "Run" button: | ||
|
||
:::code language="csharp" interactive="try-dotnet-method" source="./snippets/PatternMatching/Program.cs" id="FirstExample"::: | ||
|
||
Examine the output. You can see that each line is processed by comparing the value of the text in the first field. The preceding sample could be similarly constructed 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 to match on the values of an `enum` type. This next 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. To see it work, press the "Run" button: | ||
|
||
> [!WARNING] | ||
> Don't copy and paste. The interactive window must be reset to run the following samples. If you make a mistake, the window hangs, and you need to refresh the page to continue. | ||
|
||
:::code language="csharp" interactive="try-dotnet" source="./snippets/PatternMatching/FirstEnumExample.cs" id="IsEnumValue"::: | ||
|
||
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. The `switch` expression ensures both of those characteristics are met, which results in fewer bugs in your apps. Let's try it and experiment. Copy the following code. Replace the two `if` statements in the interactive window with the `switch` expression you copied. After you've modified the code, press the "Run" button at the top of the interactive window to run the new sample. | ||
|
||
:::code language="csharp" interactive="try-dotnet" source="./snippets/PatternMatching/EnumSwitchExample.cs" id="SwitchEnumValue"::: | ||
|
||
When you run the code, you see that it works the same. To demonstrate *subsumption*, reorder the switch arms as shown in the following snippet: | ||
|
||
```csharp | ||
currentBalance += transaction switch | ||
{ | ||
(TransactionType.Deposit, var amount) => amount, | ||
_ => 0.0, | ||
(TransactionType.Withdrawal, var amount) => -amount, | ||
}; | ||
``` | ||
|
||
After you reorder the switch arms, press the "Run" button. 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. For example, if you remove the line with `_ => 0.0,`, any invalid values don't match. At run time, that would fail. Once you install the .NET SDK and build programs in your environment, you can test this behavior. The online experience doesn't display warnings in the output window. | ||
|
||
## Type patterns | ||
|
||
To finish this tutorial, let's explore one more building block to 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. Let's modify the current sample in two ways. First, instead of a tuple, let's build `Deposit` and `Withdrawal` record types that represent the transactions. Add the following declarations at the bottom of the interactive window: | ||
|
||
:::code language="csharp" interactive="try-dotnet" source="./snippets/PatternMatching/FinalExampleProgram.cs" id="RecordDeclarations"::: | ||
|
||
Next, add this method after the `Main` method to parse the text and return a series of records: | ||
|
||
:::code language="csharp" interactive="try-dotnet" source="./snippets/PatternMatching/FinalExampleProgram.cs" id="ParseToRecord"::: | ||
|
||
Finally, replace the `foreach` loop in the `Main` method with the following code: | ||
|
||
:::code language="csharp" interactive="try-dotnet" source="./snippets/PatternMatching/FinalExampleProgram.cs" id="TypePattern"::: | ||
|
||
Then, press the "Run" button 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 more clear 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. First, visit the [.NET site](https://dotnet.microsoft.com/learn/dotnet/hello-world-tutorial/intro) to download the .NET SDK, create a project on your machine, and keep coding. As you explore, you can learn more about pattern matching in C# in the following articles: | ||
|
||
- [Pattern matching in C#](../../fundamentals/functional/pattern-matching.md) | ||
- [Explore pattern matching tutorial](../../tutorials/patterns-objects.md) | ||
- [Pattern matching scenario](../../fundamentals/tutorials/pattern-matching.md) |
76 changes: 76 additions & 0 deletions
76
docs/csharp/tour-of-csharp/tutorials/snippets/PatternMatching/EnumSwitchExample.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
namespace EnumSwitchExample; | ||
|
||
// <IsEnumValue> | ||
public static class ExampleProgram | ||
{ | ||
const 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 | ||
"""; | ||
|
||
public static void Main() | ||
{ | ||
double currentBalance = 0.0; | ||
|
||
foreach (var transaction in TransactionRecords(bankRecords)) | ||
{ | ||
// <SwitchEnumValue> | ||
currentBalance += transaction switch | ||
{ | ||
(TransactionType.Deposit, var amount) => amount, | ||
(TransactionType.Withdrawal, var amount) => -amount, | ||
_ => 0.0, | ||
}; | ||
// </SwitchEnumValue> | ||
Console.WriteLine($"{transaction.type} => Parsed Amount: {transaction.amount}, New Balance: {currentBalance}"); | ||
} | ||
} | ||
|
||
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); | ||
} | ||
yield return (TransactionType.Invalid, 0.0); | ||
} | ||
} | ||
} | ||
|
||
public enum TransactionType | ||
{ | ||
Deposit, | ||
Withdrawal, | ||
Invalid | ||
} | ||
// </IsEnumValue> |
79 changes: 79 additions & 0 deletions
79
docs/csharp/tour-of-csharp/tutorials/snippets/PatternMatching/FinalExampleProgram.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
public static class ExampleProgram | ||
{ | ||
const 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 | ||
"""; | ||
|
||
public static void Main() | ||
{ | ||
double currentBalance = 0.0; | ||
|
||
// <TypePattern> | ||
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}"); | ||
} | ||
// </TypePattern> | ||
} | ||
|
||
// <ParseToRecord> | ||
public 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; | ||
} | ||
} | ||
// </ParseToRecord> | ||
} | ||
|
||
public enum TransactionType | ||
{ | ||
Deposit, | ||
Withdrawal, | ||
Invalid | ||
} | ||
|
||
// <RecordDeclarations> | ||
public record Deposit(double Amount, string description); | ||
public record Withdrawal(double Amount, string description); | ||
// </RecordDeclarations> |
72 changes: 72 additions & 0 deletions
72
docs/csharp/tour-of-csharp/tutorials/snippets/PatternMatching/FirstEnumExample.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
namespace FirstEnumExample; | ||
|
||
// <IsEnumValue> | ||
public static class ExampleProgram | ||
{ | ||
const 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 | ||
"""; | ||
|
||
public static void Main() | ||
{ | ||
double 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}"); | ||
} | ||
} | ||
|
||
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); | ||
} | ||
yield return (TransactionType.Invalid, 0.0); | ||
} | ||
} | ||
} | ||
|
||
public enum TransactionType | ||
{ | ||
Deposit, | ||
Withdrawal, | ||
Invalid | ||
} | ||
// </IsEnumValue> |
10 changes: 10 additions & 0 deletions
10
docs/csharp/tour-of-csharp/tutorials/snippets/PatternMatching/PatternMatching.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<OutputType>Exe</OutputType> | ||
<TargetFramework>net9.0</TargetFramework> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
</PropertyGroup> | ||
|
||
</Project> |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.