diff --git a/.openpublishing.redirection.csharp.json b/.openpublishing.redirection.csharp.json index 036d6f712c3f5..8470435d8f551 100644 --- a/.openpublishing.redirection.csharp.json +++ b/.openpublishing.redirection.csharp.json @@ -34,7 +34,7 @@ }, { "source_path_from_root": "/docs/csharp/deconstruct.md", - "redirect_url": "/dotnet/csharp/fundamentals/functional/deconstruct" + "redirect_url": "/dotnet/csharp/fundamentals/patterns/deconstruct" }, { "source_path_from_root": "/docs/csharp/delegates-events.md", @@ -80,6 +80,10 @@ "source_path_from_root": "/docs/csharp/features.md", "redirect_url": "/dotnet/csharp/programming-guide/concepts" }, + { + "source_path_from_root": "/docs/csharp/fundamentals/functional/deconstruct.md", + "redirect_url": "/dotnet/csharp/fundamentals/patterns/deconstruct" + }, { "source_path_from_root": "/docs/csharp/fundamentals/functional/discards.md", "redirect_url": "/dotnet/csharp/fundamentals/patterns/discards" @@ -5540,7 +5544,7 @@ }, { "source_path_from_root": "/docs/csharp/tutorials/exploration/patterns-objects.md", - "redirect_url": "/dotnet/csharp/tutorials/patterns-objects" + "redirect_url": "/dotnet/csharp/fundamentals/tutorials/pattern-matching" }, { "source_path_from_root": "/docs/csharp/tutorials/exploration/records.md", @@ -5630,6 +5634,10 @@ "source_path_from_root": "/docs/csharp/tutorials/pattern-matching.md", "redirect_url": "/dotnet/csharp/fundamentals/tutorials/pattern-matching" }, + { + "source_path_from_root": "/docs/csharp/tutorials/patterns-objects.md", + "redirect_url": "/dotnet/csharp/fundamentals/tutorials/pattern-matching" + }, { "source_path_from_root": "/docs/csharp/tutorials/records.md", "redirect_url": "/dotnet/csharp/fundamentals/tutorials/records" @@ -5719,7 +5727,7 @@ }, { "source_path_from_root": "/docs/csharp/whats-new/tutorials/patterns-objects.md", - "redirect_url": "/dotnet/csharp/tutorials/patterns-objects" + "redirect_url": "/dotnet/csharp/fundamentals/tutorials/pattern-matching" }, { "source_path_from_root": "/docs/csharp/whats-new/tutorials/ranges-indexes.md", diff --git a/docs/csharp/advanced-topics/expression-trees/index.md b/docs/csharp/advanced-topics/expression-trees/index.md index d21593f1c863f..c305d240df990 100644 --- a/docs/csharp/advanced-topics/expression-trees/index.md +++ b/docs/csharp/advanced-topics/expression-trees/index.md @@ -64,6 +64,6 @@ Expression trees don't support new expression node types. It would be a breaking - Expressions using or , [index "from end" (`^`) operator](../../language-reference/operators/member-access-operators.md#index-from-end-operator-) or [range expressions (`..`)](../../language-reference/operators/member-access-operators.md#range-operator-) - [`async` lambda expressions or `await` expressions](../../language-reference/operators/lambda-expressions.md#async-lambdas), including [`await foreach` and `await using`](../../language-reference/operators/await.md#asynchronous-streams-and-disposables) - [Tuple literals, tuple conversions, tuple `==` or `!=`, or `with` expressions](../../language-reference/builtin-types/value-tuples.md) -- [Discards (`_`)](../../fundamentals/patterns/discards.md), [deconstructing assignment](../../fundamentals/functional/deconstruct.md), [pattern matching `is` operator, or the pattern matching `switch` expression](../../language-reference/operators/patterns.md) +- [Discards (`_`)](../../fundamentals/patterns/discards.md), [deconstructing assignment](../../fundamentals/patterns/deconstruct.md), [pattern matching `is` operator, or the pattern matching `switch` expression](../../language-reference/operators/patterns.md) - COM call with `ref` omitted on the arguments - [`ref`](../../language-reference/keywords/ref.md), [`in`](../../language-reference/keywords/method-parameters.md#in-parameter-modifier) or [`out`](../../language-reference/keywords/method-parameters.md#out-parameter-modifier) parameters, `ref` return values, `out` arguments, or any values of [`ref struct` type](../../language-reference/builtin-types/ref-struct.md) diff --git a/docs/csharp/fundamentals/expressions/operators.md b/docs/csharp/fundamentals/expressions/operators.md index 9b20b385deefa..e8b8861b99624 100644 --- a/docs/csharp/fundamentals/expressions/operators.md +++ b/docs/csharp/fundamentals/expressions/operators.md @@ -156,7 +156,7 @@ This article covers the operators you'll encounter most in everyday code. The C# - **Null operators** (`??`, `??=`, `?.`, `?[]`) — safely handle `null` values by providing defaults or short-circuiting member access: [Null operators](../null-safety/null-operators.md) - **Type-test and conversion operators** (`is`, `as`, `typeof`, cast `(T)`) — check or convert a value's runtime type: [Type-testing and cast operators](../../language-reference/operators/type-testing-and-cast.md) - **Range and index operators** (`..`, `^`) — create ranges and end-relative indexes for slicing arrays and spans: [Member access and null-conditional operators](../../language-reference/operators/member-access-operators.md) -- **Deconstruction assignment** — unpack a tuple or type into individual variables in a single expression: [Deconstructing tuples and other types](../../fundamentals/functional/deconstruct.md) +- **Deconstruction assignment** — unpack a tuple or type into individual variables in a single expression: [Deconstructing tuples and other types](../../fundamentals/patterns/deconstruct.md) ## See also diff --git a/docs/csharp/fundamentals/functional/deconstruct.md b/docs/csharp/fundamentals/functional/deconstruct.md deleted file mode 100644 index e9fcc280e460d..0000000000000 --- a/docs/csharp/fundamentals/functional/deconstruct.md +++ /dev/null @@ -1,111 +0,0 @@ ---- -title: Deconstructing tuples and other types -description: Learn how to deconstruct tuples and other types. -ms.date: 11/22/2024 ---- -# Deconstructing tuples and other types - -A tuple provides a lightweight way to retrieve multiple values from a method call. But once you retrieve the tuple, you have to handle its individual elements. Working on an element-by-element basis is cumbersome, as the following example shows. The `QueryCityData` method returns a three-tuple, and each of its elements is assigned to a variable in a separate operation. - -:::code language="csharp" source="./snippets/deconstructing-tuples/deconstruct-tuple1.cs"::: - -Retrieving multiple field and property values from an object can be equally cumbersome: you must assign a field or property value to a variable on a member-by-member basis. - -You can retrieve multiple elements from a tuple or retrieve multiple field, property, and computed values from an object in a single *deconstruct* operation. To deconstruct a tuple, you assign its elements to individual variables. When you deconstruct an object, you assign selected values to individual variables. - -## Tuples - -C# features built-in support for deconstructing tuples, which lets you unpackage all the items in a tuple in a single operation. The general syntax for deconstructing a tuple is similar to the syntax for defining one: you enclose the variables to which each element is to be assigned in parentheses in the left side of an assignment statement. For example, the following statement assigns the elements of a four-tuple to four separate variables: - -```csharp -var (name, address, city, zip) = contact.GetAddressInfo(); -``` - -There are three ways to deconstruct a tuple: - -- You can explicitly declare the type of each field inside parentheses. The following example uses this approach to deconstruct the three-tuple returned by the `QueryCityData` method. - - :::code language="csharp" source="./snippets/deconstructing-tuples/deconstruct-tuple2.cs" ID="Snippet1"::: - -- You can use the `var` keyword so that C# infers the type of each variable. You place the `var` keyword outside of the parentheses. The following example uses type inference when deconstructing the three-tuple returned by the `QueryCityData` method. - - :::code language="csharp" source="./snippets/deconstructing-tuples/deconstruct-tuple3.cs" ID="Snippet1"::: - - You can also use the `var` keyword individually with any or all of the variable declarations inside the parentheses. - - :::code language="csharp" source="./snippets/deconstructing-tuples/deconstruct-tuple4.cs" ID="Snippet1"::: - - The preceding example is cumbersome and isn't recommended. - -- Lastly, you can deconstruct the tuple into variables already declared. - - :::code language="csharp" source="./snippets/deconstructing-tuples/deconstruct-tuple5.cs" ID="Snippet1"::: - -- You can mix variable declaration and assignment in a deconstruction. - - :::code language="csharp" source="./snippets/deconstructing-tuples/deconstruct-tuple6.cs" ID="Snippet1"::: - -You can't specify a specific type outside the parentheses even if every field in the tuple has the -same type. Doing so generates compiler error CS8136, "Deconstruction 'var (...)' form disallows a specific type for 'var'." - -You must assign each element of the tuple to a variable. If you omit any elements, the compiler generates error CS8132, "Can't deconstruct a tuple of 'x' elements into 'y' variables." - -## Tuple elements with discards - -Often when deconstructing a tuple, you're interested in the values of only some elements. You can take advantage of C#'s support for *discards*, which are write-only variables whose values you chose to ignore. You declare a discard with an underscore character ("\_") in an assignment. You can discard as many values as you like; a single discard, `_`, represents all the discarded values. - -The following example illustrates the use of tuples with discards. The `QueryCityDataForYears` method returns a six-tuple with the name of a city, its area, a year, the city's population for that year, a second year, and the city's population for that second year. The example shows the change in population between those two years. Of the data available from the tuple, we're unconcerned with the city area, and we know the city name and the two dates at design-time. As a result, we're only interested in the two population values stored in the tuple, and can handle its remaining values as discards. - -:::code language="csharp" source="./snippets/deconstructing-tuples/discard-tuple1.cs"::: - -## User-defined types - -C# offers built-in support for deconstructing tuple types, [`record`](#record-types), and [DictionaryEntry](xref:System.Collections.DictionaryEntry.Deconstruct%2A) types. However, as the author of a class, a struct, or an interface, you can allow instances of the type to be deconstructed by implementing one or more `Deconstruct` methods. The method returns void. An [out](../../language-reference/keywords/method-parameters.md#out-parameter-modifier) parameter in the method signature represents each value to be deconstructed. For example, the following `Deconstruct` method of a `Person` class returns the first, middle, and family name: - -:::code language="csharp" source="./snippets/deconstructing-tuples/deconstruct-class1.cs" ID="Snippet1"::: - -You can then deconstruct an instance of the `Person` class named `p` with an assignment like the following code: - -:::code language="csharp" source="./snippets/deconstructing-tuples/deconstruct-class1.cs" ID="Snippet2"::: - -The following example overloads the `Deconstruct` method to return various combinations of properties of a `Person` object. Individual overloads return: - -- A first and family name. -- A first, middle, and family name. -- A first name, a family name, a city name, and a state name. - -:::code language="csharp" source="./snippets/deconstructing-tuples/deconstruct-class2.cs"::: - -Multiple `Deconstruct` methods having the same number of parameters are ambiguous. You must be careful to define `Deconstruct` methods with different numbers of parameters, or "arity". `Deconstruct` methods with the same number of parameters can't be distinguished during overload resolution. - -## User-defined type with discards - -Just as you do with [tuples](#tuple-elements-with-discards), you can use discards to ignore selected items returned by a `Deconstruct` method. A variable named "\_" represents a discard. A single deconstruction operation can include multiple discards. - -The following example deconstructs a `Person` object into four strings (the first and family names, the city, and the state) but discards the family name and the state. - -:::code language="csharp" source="./snippets/deconstructing-tuples/class-discard1.cs" ID="Snippet1"::: - -## Deconstruction extension methods - -If you didn't author a class, struct, or interface, you can still deconstruct objects of that type by implementing one or more `Deconstruct` [extension methods](../../programming-guide/classes-and-structs/extension-methods.md) to return the values in which you're interested. - -The following example defines two `Deconstruct` extension methods for the class. The first returns a set of values that indicate the characteristics of the property. The second indicates the property's accessibility. Boolean values indicate whether the property has separate get and set accessors or different accessibility. If there's only one accessor or both the get and the set accessor have the same accessibility, the `access` variable indicates the accessibility of the property as a whole. Otherwise, the accessibility of the get and set accessors are indicated by the `getAccess` and `setAccess` variables. - -:::code source="./snippets/deconstructing-tuples/deconstruct-extension1.cs"::: - -## Extension method for system types - -Some system types provide the `Deconstruct` method as a convenience. For example, the type provides this functionality. When you're iterating over a , each element is a `KeyValuePair` and can be deconstructed. Consider the following example: - -:::code source="./snippets/deconstructing-tuples/deconstruct-kvp.cs" id="KeyValuePair"::: - -## `record` types - -When you declare a [record](../../language-reference/builtin-types/record.md) type by using two or more positional parameters, the compiler creates a `Deconstruct` method with an `out` parameter for each positional parameter in the `record` declaration. For more information, see [Positional syntax for property definition](../../language-reference/builtin-types/record.md#positional-syntax-for-property-and-field-definition) and [Deconstructor behavior in derived records](../../language-reference/builtin-types/record.md#deconstructor-behavior-in-derived-records). - -## See also - -- [Deconstruct variable declaration (style rule IDE0042)](../../../fundamentals/code-analysis/style-rules/ide0042.md) -- [Discards](../patterns/discards.md) -- [Tuple types](../../language-reference/builtin-types/value-tuples.md) diff --git a/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/Program.cs b/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/Program.cs deleted file mode 100644 index 72f859addc661..0000000000000 --- a/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/Program.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace deconstruction -{ - class Program - { - static void Main(string[] args) - { - Example.Main(); - Example2.Main(); - Example3.Main(); - Example4.Main(); - ExampleDiscard.Main(); - ExampleClassDeconstruction.Main(); - ExampleExtension.Main(); - ExampleSystem.KeyValuePair(); - } - } -} diff --git a/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/class-discard1.cs b/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/class-discard1.cs deleted file mode 100644 index 58747a8e78c0c..0000000000000 --- a/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/class-discard1.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; - -namespace ClassDiscard -{ - public class Person - { - public string FirstName { get; set; } - public string MiddleName { get; set; } - public string LastName { get; set; } - public string City { get; set; } - public string State { get; set; } - - public Person(string fname, string mname, string lname, - string cityName, string stateName) - { - FirstName = fname; - MiddleName = mname; - LastName = lname; - City = cityName; - State = stateName; - } - - // Return the first and last name. - public void Deconstruct(out string fname, out string lname) - { - fname = FirstName; - lname = LastName; - } - - public void Deconstruct(out string fname, out string mname, out string lname) - { - fname = FirstName; - mname = MiddleName; - lname = LastName; - } - - public void Deconstruct(out string fname, out string lname, - out string city, out string state) - { - fname = FirstName; - lname = LastName; - city = City; - state = State; - } - } - - public class Example - { - public static void Main() - { - var p = new Person("John", "Quincy", "Adams", "Boston", "MA"); - - // - // Deconstruct the person object. - var (fName, _, city, _) = p; - Console.WriteLine($"Hello {fName} of {city}!"); - // The example displays the following output: - // Hello John of Boston! - // - } - } - // The example displays the following output: - // Hello John Adams of Boston, MA! -} diff --git a/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-class1.cs b/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-class1.cs deleted file mode 100644 index 79a53bc483620..0000000000000 --- a/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-class1.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace Deconstruction -{ - public class Person - { - public string FirstName { get; set; } - public string MiddleName { get; set; } - public string LastName { get; set; } - - public Person(string fname, string mname, string lname) - { - FirstName = fname; - MiddleName = mname; - LastName = lname; - } - // - public void Deconstruct(out string fname, out string mname, out string lname) - // - { - fname = FirstName; - mname = MiddleName; - lname = LastName; - } - } - - public class Example - { - public static void Main() - { - var p = new Person("John", "Quincy", "Adams"); - // - var (fName, mName, lName) = p; - // - } - } -} diff --git a/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-class2.cs b/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-class2.cs deleted file mode 100644 index 54c7f9bd08602..0000000000000 --- a/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-class2.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; - -public class Person -{ - public string FirstName { get; set; } - public string MiddleName { get; set; } - public string LastName { get; set; } - public string City { get; set; } - public string State { get; set; } - - public Person(string fname, string mname, string lname, - string cityName, string stateName) - { - FirstName = fname; - MiddleName = mname; - LastName = lname; - City = cityName; - State = stateName; - } - - // Return the first and last name. - public void Deconstruct(out string fname, out string lname) - { - fname = FirstName; - lname = LastName; - } - - public void Deconstruct(out string fname, out string mname, out string lname) - { - fname = FirstName; - mname = MiddleName; - lname = LastName; - } - - public void Deconstruct(out string fname, out string lname, - out string city, out string state) - { - fname = FirstName; - lname = LastName; - city = City; - state = State; - } -} - -public class ExampleClassDeconstruction -{ - public static void Main() - { - var p = new Person("John", "Quincy", "Adams", "Boston", "MA"); - - // Deconstruct the person object. - var (fName, lName, city, state) = p; - Console.WriteLine($"Hello {fName} {lName} of {city}, {state}!"); - } -} -// The example displays the following output: -// Hello John Adams of Boston, MA! diff --git a/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-extension1.cs b/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-extension1.cs deleted file mode 100644 index 2ae1b453546fa..0000000000000 --- a/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-extension1.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; - -public static class ReflectionExtensions -{ - extension(PropertyInfo propertyInfo) - { - public void Deconstruct(out bool isStatic, - out bool isReadOnly, out bool isIndexed, - out Type propertyType) - { - var getter = propertyInfo.GetMethod; - - // Is the property read-only? - isReadOnly = ! propertyInfo.CanWrite; - - // Is the property instance or static? - isStatic = getter.IsStatic; - - // Is the property indexed? - isIndexed = propertyInfo.GetIndexParameters().Length > 0; - - // Get the property type. - propertyType = propertyInfo.PropertyType; - } - - public void Deconstruct(out bool hasGetAndSet, - out bool sameAccess, out string access, - out string getAccess, out string setAccess) - { - hasGetAndSet = sameAccess = false; - string getAccessTemp = null; - string setAccessTemp = null; - - MethodInfo getter = null; - if (propertyInfo.CanRead) - getter = propertyInfo.GetMethod; - - MethodInfo setter = null; - if (propertyInfo.CanWrite) - setter = propertyInfo.SetMethod; - - if (setter != null && getter != null) - hasGetAndSet = true; - - if (getter != null) - { - if (getter.IsPublic) - getAccessTemp = "public"; - else if (getter.IsPrivate) - getAccessTemp = "private"; - else if (getter.IsAssembly) - getAccessTemp = "internal"; - else if (getter.IsFamily) - getAccessTemp = "protected"; - else if (getter.IsFamilyOrAssembly) - getAccessTemp = "protected internal"; - } - - if (setter != null) - { - if (setter.IsPublic) - setAccessTemp = "public"; - else if (setter.IsPrivate) - setAccessTemp = "private"; - else if (setter.IsAssembly) - setAccessTemp = "internal"; - else if (setter.IsFamily) - setAccessTemp = "protected"; - else if (setter.IsFamilyOrAssembly) - setAccessTemp = "protected internal"; - } - - // Are the accessibility of the getter and setter the same? - if (setAccessTemp == getAccessTemp) - { - sameAccess = true; - access = getAccessTemp; - getAccess = setAccess = String.Empty; - } - else - { - access = null; - getAccess = getAccessTemp; - setAccess = setAccessTemp; - } - } - } -} - -public class ExampleExtension -{ - public static void Main() - { - Type dateType = typeof(DateTime); - PropertyInfo prop = dateType.GetProperty("Now"); - var (isStatic, isRO, isIndexed, propType) = prop; - Console.WriteLine($"\nThe {dateType.FullName}.{prop.Name} property:"); - Console.WriteLine($" PropertyType: {propType.Name}"); - Console.WriteLine($" Static: {isStatic}"); - Console.WriteLine($" Read-only: {isRO}"); - Console.WriteLine($" Indexed: {isIndexed}"); - - Type listType = typeof(List<>); - prop = listType.GetProperty("Item", - BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); - var (hasGetAndSet, sameAccess, accessibility, getAccessibility, setAccessibility) = prop; - Console.Write($"\nAccessibility of the {listType.FullName}.{prop.Name} property: "); - - if (!hasGetAndSet | sameAccess) - { - Console.WriteLine(accessibility); - } - else - { - Console.WriteLine($"\n The get accessor: {getAccessibility}"); - Console.WriteLine($" The set accessor: {setAccessibility}"); - } - } -} -// The example displays the following output: -// The System.DateTime.Now property: -// PropertyType: DateTime -// Static: True -// Read-only: True -// Indexed: False -// -// Accessibility of the System.Collections.Generic.List`1.Item property: public diff --git a/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-kvp.cs b/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-kvp.cs deleted file mode 100644 index 14b5f50f0aabe..0000000000000 --- a/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-kvp.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Collections.Generic; - -public class ExampleSystem -{ - public static void KeyValuePair() - { - // - Dictionary snapshotCommitMap = new(StringComparer.OrdinalIgnoreCase) - { - ["https://github.com/dotnet/docs"] = 16_465, - ["https://github.com/dotnet/runtime"] = 114_223, - ["https://github.com/dotnet/installer"] = 22_436, - ["https://github.com/dotnet/roslyn"] = 79_484, - ["https://github.com/dotnet/aspnetcore"] = 48_386 - }; - - foreach (var (repo, commitCount) in snapshotCommitMap) - { - Console.WriteLine( - $"The {repo} repository had {commitCount:N0} commits as of November 10th, 2021."); - } - // - } -} diff --git a/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-tuple1.cs b/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-tuple1.cs deleted file mode 100644 index 977c1da53e018..0000000000000 --- a/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-tuple1.cs +++ /dev/null @@ -1,21 +0,0 @@ -public class Example -{ - public static void Main() - { - var result = QueryCityData("New York City"); - - var city = result.Item1; - var pop = result.Item2; - var size = result.Item3; - - // Do something with the data. - } - - private static (string, int, double) QueryCityData(string name) - { - if (name == "New York City") - return (name, 8175133, 468.48); - - return ("", 0, 0); - } -} diff --git a/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-tuple2.cs b/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-tuple2.cs deleted file mode 100644 index 93a78b35494c6..0000000000000 --- a/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-tuple2.cs +++ /dev/null @@ -1,19 +0,0 @@ -public class Example2 -{ - // - public static void Main() - { - (string city, int population, double area) = QueryCityData("New York City"); - - // Do something with the data. - } - // - - private static (string, int, double) QueryCityData(string name) - { - if (name == "New York City") - return (name, 8175133, 468.48); - - return ("", 0, 0); - } -} diff --git a/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-tuple3.cs b/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-tuple3.cs deleted file mode 100644 index 4230aaa10e2fe..0000000000000 --- a/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-tuple3.cs +++ /dev/null @@ -1,19 +0,0 @@ -public class Example3 -{ - // - public static void Main() - { - var (city, population, area) = QueryCityData("New York City"); - - // Do something with the data. - } - // - - private static (string, int, double) QueryCityData(string name) - { - if (name == "New York City") - return (name, 8175133, 468.48); - - return ("", 0, 0); - } -} diff --git a/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-tuple4.cs b/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-tuple4.cs deleted file mode 100644 index 7384ded039242..0000000000000 --- a/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-tuple4.cs +++ /dev/null @@ -1,19 +0,0 @@ -public class Example4 -{ - // - public static void Main() - { - (string city, var population, var area) = QueryCityData("New York City"); - - // Do something with the data. - } - // - - private static (string, int, double) QueryCityData(string name) - { - if (name == "New York City") - return (name, 8175133, 468.48); - - return ("", 0, 0); - } -} diff --git a/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-tuple5.cs b/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-tuple5.cs deleted file mode 100644 index 231f3c4fbac31..0000000000000 --- a/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-tuple5.cs +++ /dev/null @@ -1,23 +0,0 @@ -public class Example5 -{ - // - public static void Main() - { - string city = "Raleigh"; - int population = 458880; - double area = 144.8; - - (city, population, area) = QueryCityData("New York City"); - - // Do something with the data. - } - // - - private static (string, int, double) QueryCityData(string name) - { - if (name == "New York City") - return (name, 8175133, 468.48); - - return ("", 0, 0); - } -} diff --git a/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-tuple6.cs b/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-tuple6.cs deleted file mode 100644 index e1185f61383c4..0000000000000 --- a/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruct-tuple6.cs +++ /dev/null @@ -1,22 +0,0 @@ -public class Example6 -{ - // - public static void Main() - { - string city = "Raleigh"; - int population = 458880; - - (city, population, double area) = QueryCityData("New York City"); - - // Do something with the data. - } - // - - private static (string, int, double) QueryCityData(string name) - { - if (name == "New York City") - return (name, 8175133, 468.48); - - return ("", 0, 0); - } -} diff --git a/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruction.csproj b/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruction.csproj deleted file mode 100644 index d816b6e17d787..0000000000000 --- a/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/deconstruction.csproj +++ /dev/null @@ -1,10 +0,0 @@ - - - - Exe - net10.0 - enable - deconstruction.Program - - - diff --git a/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/discard-tuple1.cs b/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/discard-tuple1.cs deleted file mode 100644 index 52d322d7d7854..0000000000000 --- a/docs/csharp/fundamentals/functional/snippets/deconstructing-tuples/discard-tuple1.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; - -public class ExampleDiscard -{ - public static void Main() - { - var (_, _, _, pop1, _, pop2) = QueryCityDataForYears("New York City", 1960, 2010); - - Console.WriteLine($"Population change, 1960 to 2010: {pop2 - pop1:N0}"); - } - - private static (string, double, int, int, int, int) QueryCityDataForYears(string name, int year1, int year2) - { - int population1 = 0, population2 = 0; - double area = 0; - - if (name == "New York City") - { - area = 468.48; - if (year1 == 1960) - { - population1 = 7781984; - } - if (year2 == 2010) - { - population2 = 8175133; - } - return (name, area, year1, population1, year2, population2); - } - - return ("", 0, 0, 0, 0, 0); - } -} -// The example displays the following output: -// Population change, 1960 to 2010: 393,149 diff --git a/docs/csharp/fundamentals/patterns/deconstruct.md b/docs/csharp/fundamentals/patterns/deconstruct.md new file mode 100644 index 0000000000000..6bb238fcb83ad --- /dev/null +++ b/docs/csharp/fundamentals/patterns/deconstruct.md @@ -0,0 +1,92 @@ +--- +title: Deconstructing tuples and other types +description: Learn how C# deconstruction assigns tuple elements or object components to individual variables. +ms.date: 09/29/2026 +ms.topic: concept-article +ai-usage: ai-assisted +--- + +# Deconstructing tuples and other types + +> [!TIP] +> This article is part of the **Fundamentals** section for developers who already know at least one programming language and are learning C#. Start with the [pattern matching overview](pattern-matching.md) if patterns are new to you. Deconstruction also enables positional patterns. For guidance on when to choose property patterns or positional patterns, see [Property and positional patterns](property-positional-patterns.md). + +A *deconstruction* assigns components from one value to multiple variables in a single operation. Tuples expose their components by position. Another type can expose components by defining a `Deconstruct` method. Positional records include one automatically. + +Deconstruction is related to pattern matching because a `Deconstruct` method also enables positional patterns for that type. Even so, property patterns are usually clearer for object shapes because member names explain the test. Positional patterns are strongest when order already carries the meaning, such as with tuples or other small ordered values. + +## Deconstruct tuples + +Suppose a method returns a tuple with city data. You can read each component one at a time: + +:::code language="csharp" source="snippets/patterns/DeconstructSamples.cs" ID="TupleMemberAccess"::: + +A deconstruction assigns those components in one step: + +:::code language="csharp" source="snippets/patterns/DeconstructSamples.cs" ID="TupleTypedDeclaration"::: + +You can also let C# infer the variable types: + +:::code language="csharp" source="snippets/patterns/DeconstructSamples.cs" ID="TupleVarDeconstruction"::: + +A deconstruction can mix existing variables, newly declared variables, and discards in one assignment: + +:::code language="csharp" source="snippets/patterns/DeconstructSamples.cs" ID="MixedDeconstruction"::: + +Choose the form that makes the code easiest to read. A single `var` before the parentheses is often the clearest inferred form. You can also mix explicit types and `var` inside the parentheses, but that form is usually harder to scan. If you need only some values, use discards instead of omitting positions. + +## Ignore unneeded values with discards + +Every produced value must line up with a position on the left side of the assignment. When you do not need one or more positions, use `_` as a discard: + +:::code language="csharp" source="snippets/patterns/DeconstructSamples.cs" ID="TupleDiscards"::: + +Here, the tuple returns the city name, two years, and two population values. The deconstruction keeps only the population values because the calculation uses only those components. + +## Deconstruct user-defined types + +A class, struct, or interface can support deconstruction by declaring a `Deconstruct` method. Each produced value is an `out` parameter. The method itself returns `void`: + +:::code language="csharp" source="snippets/patterns/DeconstructSamples.cs" ID="PersonDeconstructMethod"::: + +You can then deconstruct an instance directly: + +:::code language="csharp" source="snippets/patterns/DeconstructSamples.cs" ID="PersonDeconstructUse"::: + +A type can provide multiple `Deconstruct` overloads with different arities so callers can choose how many components to retrieve: + +:::code language="csharp" source="snippets/patterns/DeconstructSamples.cs" ID="PersonDeconstructOverloads"::: + +Two overloads with the same number of `out` parameters are ambiguous. Distinguish overloads by arity, not only by parameter types. + +Discards work with user-defined deconstruction too: + +:::code language="csharp" source="snippets/patterns/DeconstructSamples.cs" ID="PersonDeconstructDiscards"::: + +## Deconstruct records + +A positional `record` or `record struct` gets a compiler-generated `Deconstruct` method whose `out` parameters match the positional parameters: + +:::code language="csharp" source="snippets/patterns/DeconstructSamples.cs" ID="RecordDeconstruction"::: + +Only the positional parameters participate in that generated deconstruction. Additional properties you declare elsewhere on the record are not added automatically. + +## Deconstruct types you don't own + +If you cannot modify a type, you can still support deconstruction by writing an extension method. After you add the method, any `Uri` value can use deconstruction syntax: + +:::code language="csharp" source="snippets/patterns/DeconstructSamples.cs" ID="UriDeconstructExample"::: + +## Use built-in deconstruction on system types + +Some system types already define `Deconstruct`. For example, supports deconstruction, which makes dictionary iteration concise: + +:::code language="csharp" source="snippets/patterns/DeconstructSamples.cs" ID="KeyValuePair"::: + +## See also + +- [Pattern matching overview](pattern-matching.md) +- [Property and positional patterns](property-positional-patterns.md) +- [Discards and the discard pattern](discards.md) +- [Tuple types](../types/tuples.md) +- [`out` parameter modifier](../../language-reference/keywords/method-parameters.md#out-parameter-modifier) diff --git a/docs/csharp/fundamentals/patterns/discards.md b/docs/csharp/fundamentals/patterns/discards.md index 1befc35bf4200..ab5b4cc6a3417 100644 --- a/docs/csharp/fundamentals/patterns/discards.md +++ b/docs/csharp/fundamentals/patterns/discards.md @@ -39,7 +39,7 @@ The form `var _` is a `var` pattern with a discard designation. It also matches :::code language="csharp" source="snippets/discards/Program.cs" ID="TupleDiscards"::: -The same discard syntax works when an object's `Deconstruct` method produces several values. For those forms, see [Deconstructing tuples and other types](../functional/deconstruct.md). +The same discard syntax works when an object's `Deconstruct` method produces several values. For those forms, see [Deconstructing tuples and other types](deconstruct.md). ## Calls to methods with `out` parameters @@ -70,6 +70,6 @@ Choose discard parameters when a delegate signature requires inputs that the lam - [Pattern matching overview](pattern-matching.md) - [Declaration, constant, and `var` patterns](declaration-constant-var-patterns.md) -- [Deconstructing tuples and other types](../functional/deconstruct.md) +- [Deconstructing tuples and other types](deconstruct.md) - [Lambda expression parameters](../../language-reference/operators/lambda-expressions.md#input-parameters-of-a-lambda-expression) - [Discard pattern reference](../../language-reference/operators/patterns.md#discard-pattern) diff --git a/docs/csharp/fundamentals/patterns/property-positional-patterns.md b/docs/csharp/fundamentals/patterns/property-positional-patterns.md index 2ae70a69236af..334ca51fdb848 100644 --- a/docs/csharp/fundamentals/patterns/property-positional-patterns.md +++ b/docs/csharp/fundamentals/patterns/property-positional-patterns.md @@ -16,7 +16,7 @@ Property and positional patterns both test parts of a value. The difference is h - A *property pattern* names the properties or fields to test. - A *positional pattern* identifies values by their order. -A *deconstruction* exposes an ordered set of component values. A tuple already has an element order; see [deconstruct tuples](../types/tuples.md#deconstruct-tuples). For another type, a [`Deconstruct` method](../functional/deconstruct.md#user-defined-types) defines which component values are exposed and their order. +A *deconstruction* exposes an ordered set of component values. A tuple already has an element order; see [deconstruct tuples](../types/tuples.md#deconstruct-tuples). For another type, a [`Deconstruct` method](deconstruct.md#deconstruct-user-defined-types) defines which component values are exposed and their order. ## Compare names and positions @@ -73,6 +73,6 @@ The pattern-based version keeps the possible results together when several branc - [Pattern matching overview](pattern-matching.md) - [Relational, logical, and parenthesized patterns](relational-logical-patterns.md). -- [Deconstructing tuples and other types](../functional/deconstruct.md) +- [Deconstructing tuples and other types](deconstruct.md) - [Property pattern reference](../../language-reference/operators/patterns.md#property-pattern). - [Positional pattern reference](../../language-reference/operators/patterns.md#positional-pattern). diff --git a/docs/csharp/fundamentals/patterns/snippets/patterns/DeconstructSamples.cs b/docs/csharp/fundamentals/patterns/snippets/patterns/DeconstructSamples.cs new file mode 100644 index 0000000000000..4bfa03549593c --- /dev/null +++ b/docs/csharp/fundamentals/patterns/snippets/patterns/DeconstructSamples.cs @@ -0,0 +1,238 @@ +static class DeconstructSamples +{ + public static void Run() + { + ShowTupleMemberAccess(); + ShowTypedTupleDeconstruction(); + ShowVarTupleDeconstruction(); + ShowMixedDeconstruction(); + ShowTupleDiscards(); + ShowPersonDeconstruction(); + ShowPersonDeconstructionWithDiscards(); + ShowRecordDeconstruction(); + ShowUriDeconstruction(); + ShowKeyValuePairDeconstruction(); + } + + static void ShowTupleMemberAccess() + { + // + var cityData = QueryCityData("New York City"); + var city = cityData.City; + var population = cityData.Population; + var area = cityData.Area; + // + + Console.WriteLine($"{city}: population {population:N0}, area {area:F2}"); + } + + static void ShowTypedTupleDeconstruction() + { + // + (string city, int population, double area) = QueryCityData("New York City"); + // + + Console.WriteLine($"{city}: population {population:N0}, area {area:F2}"); + } + + static void ShowVarTupleDeconstruction() + { + // + var (city, population, area) = QueryCityData("New York City"); + // + + Console.WriteLine($"{city}: population {population:N0}, area {area:F2}"); + } + + static void ShowMixedDeconstruction() + { + string city = "Raleigh"; + + // + (city, var population, _) = QueryCityData("New York City"); + // + + Console.WriteLine($"{city}: population {population:N0}"); + } + + static void ShowTupleDiscards() + { + // + var (_, _, population1960, _, population2010) = QueryPopulationDataForYears( + "New York City", 1960, 2010); + // + + Console.WriteLine($"Population change: {population2010 - population1960:N0}"); + } + + static (string City, int Population, double Area) QueryCityData(string name) => + name switch + { + "New York City" => (name, 8_175_133, 468.48), + "Raleigh" => (name, 458_880, 149.60), + _ => (name, 0, 0) + }; + + static (string City, int Year1, int PopulationYear1, int Year2, int PopulationYear2) QueryPopulationDataForYears( + string name, int year1, int year2) => + (name, year1, year2) switch + { + ("New York City", 1960, 2010) => (name, year1, 7_781_984, year2, 8_175_133), + _ => (name, year1, 0, year2, 0) + }; + + static void ShowPersonDeconstruction() + { + var passenger = new Person("John", "Quincy", "Adams", "Boston", "MA"); + + // + var (firstName, middleName, lastName) = passenger; + // + + Console.WriteLine($"{firstName} {middleName} {lastName}"); + } + + static void ShowPersonDeconstructionWithDiscards() + { + var passenger = new Person("John", "Quincy", "Adams", "Boston", "MA"); + + // + var (firstName, _, city, _) = passenger; + // + + Console.WriteLine($"{firstName} from {city}"); + } + + static void ShowRecordDeconstruction() + { + var forecast = new Forecast("Redmond", 18, 9); + + // + var (city, highTempC, lowTempC) = forecast; + // + + Console.WriteLine($"{city}: {highTempC}C / {lowTempC}C"); + } + + static void ShowUriDeconstruction() + { + var docsSite = new Uri("https://learn.microsoft.com:443/dotnet/csharp/"); + var (scheme, host, port) = docsSite; + Console.WriteLine($"{scheme}://{host}:{port}"); + } + + static void ShowKeyValuePairDeconstruction() + { + Dictionary repoCommitCounts = new(StringComparer.OrdinalIgnoreCase) + { + ["https://github.com/dotnet/docs"] = 16_465, + ["https://github.com/dotnet/runtime"] = 114_223, + ["https://github.com/dotnet/roslyn"] = 79_484, + }; + + // + foreach (var (repo, commitCount) in repoCommitCounts) + { + Console.WriteLine($"{repo} had {commitCount:N0} commits in this snapshot."); + } + // + } +} + +sealed class Person +{ + public Person(string firstName, string middleName, string lastName, string city, string state) + { + FirstName = firstName; + MiddleName = middleName; + LastName = lastName; + City = city; + State = state; + } + + public string FirstName { get; } + + public string MiddleName { get; } + + public string LastName { get; } + + public string City { get; } + + public string State { get; } + + // + public void Deconstruct(out string firstName, out string middleName, out string lastName) + { + firstName = FirstName; + middleName = MiddleName; + lastName = LastName; + } + // + + public void Deconstruct(out string firstName, out string lastName, out string city, out string state) + { + firstName = FirstName; + lastName = LastName; + city = City; + state = State; + } +} + +readonly record struct Forecast(string City, int HighTempC, int LowTempC); + +sealed class Traveler +{ + public Traveler(string firstName, string middleName, string lastName, string city, string state) + { + FirstName = firstName; + MiddleName = middleName; + LastName = lastName; + City = city; + State = state; + } + + public string FirstName { get; } + + public string MiddleName { get; } + + public string LastName { get; } + + public string City { get; } + + public string State { get; } + + // + public void Deconstruct(out string firstName, out string lastName) + { + firstName = FirstName; + lastName = LastName; + } + + public void Deconstruct(out string firstName, out string middleName, out string lastName) + { + firstName = FirstName; + middleName = MiddleName; + lastName = LastName; + } + + public void Deconstruct(out string firstName, out string lastName, out string city, out string state) + { + firstName = FirstName; + lastName = LastName; + city = City; + state = State; + } + // +} + +// +static class UriExtensions +{ + public static void Deconstruct(this Uri uri, out string scheme, out string host, out int port) + { + scheme = uri.Scheme; + host = uri.Host; + port = uri.Port; + } +} +// diff --git a/docs/csharp/fundamentals/patterns/snippets/patterns/Program.cs b/docs/csharp/fundamentals/patterns/snippets/patterns/Program.cs index 8aea8ed7ab1e2..7d219854a5b14 100644 --- a/docs/csharp/fundamentals/patterns/snippets/patterns/Program.cs +++ b/docs/csharp/fundamentals/patterns/snippets/patterns/Program.cs @@ -2,5 +2,6 @@ BasicPatterns.Run(); TypePatterns.Run(); PropertyPositionalPatterns.Run(); +DeconstructSamples.Run(); RelationalLogicalPatterns.Run(); ListPatterns.Run(); diff --git a/docs/csharp/fundamentals/tutorials/advanced/pattern-matching.md b/docs/csharp/fundamentals/tutorials/advanced/pattern-matching.md new file mode 100644 index 0000000000000..cfc2146508e62 --- /dev/null +++ b/docs/csharp/fundamentals/tutorials/advanced/pattern-matching.md @@ -0,0 +1,380 @@ +--- +title: "Tutorial: Build algorithms with pattern matching" +description: This advanced tutorial demonstrates how to use pattern matching techniques to create functionality using data and algorithms that are created separately. +ms.date: 02/25/2022 +--- +# Tutorial: Use pattern matching to build type-driven and data-driven algorithms + +You can write functionality that behaves as though you extended types that may be in other libraries. Another use for patterns is to create functionality your application requires that isn't a fundamental feature of the type being extended. + +In this tutorial, you'll learn how to: + +> [!div class="checklist"] +> +> - Recognize situations where pattern matching should be used. +> - Use pattern matching expressions to implement behavior based on types and property values. +> - Combine pattern matching with other techniques to create complete algorithms. + +## Prerequisites + +[!INCLUDE [Prerequisites](../../../../../includes/prerequisites-basic-winget.md)] + +This tutorial assumes you're familiar with C# and .NET, including either Visual Studio or the .NET CLI. + +## Scenarios for pattern matching + +Modern development often includes integrating data from multiple sources and presenting information and insights from that data in a single cohesive application. You and your team won't have control or access for all the types that represent the incoming data. + +The classic object-oriented design would call for creating types in your application that represent each data type from those multiple data sources. Then, your application would work with those new types, build inheritance hierarchies, create virtual methods, and implement abstractions. Those techniques work, and sometimes they're the best tools. Other times you can write less code. You can write more clear code using techniques that separate the data from the operations that manipulate that data. + +In this tutorial, you'll create and explore an application that takes incoming data from several external sources for a single scenario. You'll see how **pattern matching** provides an efficient way to consume and process that data in ways that weren't part of the original system. + +Consider a major metropolitan area that is using tolls and peak time pricing to manage traffic. You write an application that calculates tolls for a vehicle based on its type. Later enhancements incorporate pricing based on the number of occupants in the vehicle. Further enhancements add pricing based on the time and the day of the week. + +From that brief description, you may have quickly sketched out an object hierarchy to model this system. However, your data is coming from multiple sources like other vehicle registration management systems. These systems provide different classes to model that data and you don't have a single object model you can use. In this tutorial, you'll use these simplified classes to model for the vehicle data from these external systems, as shown in the following code: + +:::code language="csharp" source="../snippets/patterns/start/toll-calculator/ExternalSystems.cs"::: + +You can download the starter code from the [dotnet/samples](https://github.com/dotnet/samples/tree/main/csharp/tutorials/patterns/start) GitHub repository. You can see that the vehicle classes are from different systems, and are in different namespaces. No common base class, other than `System.Object` can be used. + +## Pattern matching designs + +The scenario used in this tutorial highlights the kinds of problems that pattern matching is well suited to solve: + +- The objects you need to work with aren't in an object hierarchy that matches your goals. You may be working with classes that are part of unrelated systems. +- The functionality you're adding isn't part of the core abstraction for these classes. The toll paid by a vehicle *changes* for different types of vehicles, but the toll isn't a core function of the vehicle. + +When the *shape* of the data and the *operations* on that data aren't described together, the pattern matching features in C# make it easier to work with. + +## Implement the basic toll calculations + +The most basic toll calculation relies only on the vehicle type: + +- A `Car` is $2.00. +- A `Taxi` is $3.50. +- A `Bus` is $5.00. +- A `DeliveryTruck` is $10.00 + +Create a new `TollCalculator` class, and implement pattern matching on the vehicle type to get the toll amount. The following code shows the initial implementation of the `TollCalculator`. + +```csharp +using System; +using CommercialRegistration; +using ConsumerVehicleRegistration; +using LiveryRegistration; + +namespace Calculators; + +public class TollCalculator +{ + public decimal CalculateToll(object vehicle) => + vehicle switch + { + Car c => 2.00m, + Taxi t => 3.50m, + Bus b => 5.00m, + DeliveryTruck t => 10.00m, + { } => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)), + null => throw new ArgumentNullException(nameof(vehicle)) + }; +} +``` + +The preceding code uses a [`switch` expression](../../../language-reference/operators/switch-expression.md) (not the same as a [`switch` statement](../../../language-reference/statements/selection-statements.md#the-switch-statement)) that tests the [declaration pattern](../../../language-reference/operators/patterns.md#declaration-and-type-patterns). A **switch expression** begins with the variable, `vehicle` in the preceding code, followed by the `switch` keyword. Next comes all the **switch arms** inside curly braces. The `switch` expression makes other refinements to the syntax that surrounds the `switch` statement. The `case` keyword is omitted, and the result of each arm is an expression. The last two arms show a new language feature. The `{ }` case matches any non-null object that didn't match an earlier arm. This arm catches any incorrect types passed to this method. The `{ }` case must follow the cases for each vehicle type. If the order were reversed, the `{ }` case would take precedence. Finally, the `null` [constant pattern](../../../language-reference/operators/patterns.md#constant-pattern) detects when `null` is passed to this method. The `null` pattern can be last because the other patterns match only a non-null object of the correct type. + +You can test this code using the following code in `Program.cs`: + +```csharp +using System; +using CommercialRegistration; +using ConsumerVehicleRegistration; +using LiveryRegistration; + +using toll_calculator; + +var tollCalc = new TollCalculator(); + +var car = new Car(); +var taxi = new Taxi(); +var bus = new Bus(); +var truck = new DeliveryTruck(); + +Console.WriteLine($"The toll for a car is {tollCalc.CalculateToll(car)}"); +Console.WriteLine($"The toll for a taxi is {tollCalc.CalculateToll(taxi)}"); +Console.WriteLine($"The toll for a bus is {tollCalc.CalculateToll(bus)}"); +Console.WriteLine($"The toll for a truck is {tollCalc.CalculateToll(truck)}"); + +try +{ + tollCalc.CalculateToll("this will fail"); +} +catch (ArgumentException e) +{ + Console.WriteLine("Caught an argument exception when using the wrong type"); +} +try +{ + tollCalc.CalculateToll(null!); +} +catch (ArgumentNullException e) +{ + Console.WriteLine("Caught an argument exception when using null"); +} +``` + +That code is included in the starter project, but is commented out. Remove the comments, and you can test what you've written. + +You're starting to see how patterns can help you create algorithms where the code and the data are separate. The `switch` expression tests the type and produces different values based on the results. That's only the beginning. + +## Add occupancy pricing + +The toll authority wants to encourage vehicles to travel at maximum capacity. They've decided to charge more when vehicles have fewer passengers, and encourage full vehicles by offering lower pricing: + +- Cars and taxis with no passengers pay an extra $0.50. +- Cars and taxis with two passengers get a $0.50 discount. +- Cars and taxis with three or more passengers get a $1.00 discount. +- Buses that are less than 50% full pay an extra $2.00. +- Buses that are more than 90% full get a $1.00 discount. + +These rules can be implemented using a [property pattern](../../language-reference/operators/patterns.md#property-pattern) in the same switch expression. A property pattern compares a property value to a constant value. The property pattern examines properties of the object once the type has been determined. The single case for a `Car` expands to four different cases: + +```csharp +vehicle switch +{ + Car {Passengers: 0} => 2.00m + 0.50m, + Car {Passengers: 1} => 2.0m, + Car {Passengers: 2} => 2.0m - 0.50m, + Car => 2.00m - 1.0m, + + // ... +}; +``` + +The first three cases test the type as a `Car`, then check the value of the `Passengers` property. If both match, that expression is evaluated and returned. + +You would also expand the cases for taxis in a similar manner: + +```csharp +vehicle switch +{ + // ... + + Taxi {Fares: 0} => 3.50m + 1.00m, + Taxi {Fares: 1} => 3.50m, + Taxi {Fares: 2} => 3.50m - 0.50m, + Taxi => 3.50m - 1.00m, + + // ... +}; +``` + +Next, implement the occupancy rules by expanding the cases for buses, as shown in the following example: + +```csharp +vehicle switch +{ + // ... + + Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m, + Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m, + Bus => 5.00m, + + // ... +}; +``` + +The toll authority isn't concerned with the number of passengers in the delivery trucks. Instead, they adjust the toll amount based on the weight class of the trucks as follows: + +- Trucks over 5000 lbs are charged an extra $5.00. +- Light trucks under 3000 lbs are given a $2.00 discount. + +That rule is implemented with the following code: + +```csharp +vehicle switch +{ + // ... + + DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m, + DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m, + DeliveryTruck => 10.00m, +}; +``` + +The preceding code shows the `when` clause of a switch arm. You use the `when` clause to test conditions other than equality on a property. When you've finished, you'll have a method that looks much like the following code: + +```csharp +vehicle switch +{ + Car {Passengers: 0} => 2.00m + 0.50m, + Car {Passengers: 1} => 2.0m, + Car {Passengers: 2} => 2.0m - 0.50m, + Car => 2.00m - 1.0m, + + Taxi {Fares: 0} => 3.50m + 1.00m, + Taxi {Fares: 1} => 3.50m, + Taxi {Fares: 2} => 3.50m - 0.50m, + Taxi => 3.50m - 1.00m, + + Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m, + Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m, + Bus => 5.00m, + + DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m, + DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m, + DeliveryTruck => 10.00m, + + { } => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)), + null => throw new ArgumentNullException(nameof(vehicle)) +}; +``` + +Many of these switch arms are examples of **recursive patterns**. For example, `Car { Passengers: 1}` shows a constant pattern inside a property pattern. + +You can make this code less repetitive by using nested switches. The `Car` and `Taxi` both have four different arms in the preceding examples. In both cases, you can create a declaration pattern that feeds into a constant pattern. This technique is shown in the following code: + +```csharp +public decimal CalculateToll(object vehicle) => + vehicle switch + { + Car c => c.Passengers switch + { + 0 => 2.00m + 0.5m, + 1 => 2.0m, + 2 => 2.0m - 0.5m, + _ => 2.00m - 1.0m + }, + + Taxi t => t.Fares switch + { + 0 => 3.50m + 1.00m, + 1 => 3.50m, + 2 => 3.50m - 0.50m, + _ => 3.50m - 1.00m + }, + + Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m, + Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m, + Bus b => 5.00m, + + DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m, + DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m, + DeliveryTruck t => 10.00m, + + { } => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)), + null => throw new ArgumentNullException(nameof(vehicle)) + }; +``` + +In the preceding sample, using a recursive expression means you don't repeat the `Car` and `Taxi` arms containing child arms that test the property value. This technique isn't used for the `Bus` and `DeliveryTruck` arms because those arms are testing ranges for the property, not discrete values. + +## Add peak pricing + +For the final feature, the toll authority wants to add time sensitive peak pricing. During the morning and evening rush hours, the tolls are doubled. That rule only affects traffic in one direction: inbound to the city in the morning, and outbound in the evening rush hour. During other times during the workday, tolls increase by 50%. Late night and early morning, tolls are reduced by 25%. During the weekend, it's the normal rate, regardless of the time. You could use a series of `if` and `else` statements to express this using the following code: + +:::code language="csharp" source="../snippets/patterns/finished/toll-calculator/TollCalculator.cs" id="SnippetPremiumWithoutPattern"::: + +The preceding code does work correctly, but isn't readable. You have to chain through all the input cases and the nested `if` statements to reason about the code. Instead, you'll use pattern matching for this feature, but you'll integrate it with other techniques. You could build a single pattern match expression that would account for all the combinations of direction, day of the week, and time. The result would be a complicated expression. It would be hard to read and difficult to understand. That makes it hard to ensure correctness. Instead, combine those methods to build a tuple of values that concisely describes all those states. Then use pattern matching to calculate a multiplier for the toll. The tuple contains three discrete conditions: + +- The day is either a weekday or a weekend. +- The band of time when the toll is collected. +- The direction is into the city or out of the city + +The following table shows the combinations of input values and the peak pricing multiplier: + +| Day | Time | Direction | Premium | +| ---------- | ------------ | --------- |--------:| +| Weekday | morning rush | inbound | x 2.00 | +| Weekday | morning rush | outbound | x 1.00 | +| Weekday | daytime | inbound | x 1.50 | +| Weekday | daytime | outbound | x 1.50 | +| Weekday | evening rush | inbound | x 1.00 | +| Weekday | evening rush | outbound | x 2.00 | +| Weekday | overnight | inbound | x 0.75 | +| Weekday | overnight | outbound | x 0.75 | +| Weekend | morning rush | inbound | x 1.00 | +| Weekend | morning rush | outbound | x 1.00 | +| Weekend | daytime | inbound | x 1.00 | +| Weekend | daytime | outbound | x 1.00 | +| Weekend | evening rush | inbound | x 1.00 | +| Weekend | evening rush | outbound | x 1.00 | +| Weekend | overnight | inbound | x 1.00 | +| Weekend | overnight | outbound | x 1.00 | + +There are 16 different combinations of the three variables. By combining some of the conditions, you'll simplify the final switch expression. + +The system that collects the tolls uses a structure for the time when the toll was collected. Build member methods that create the variables from the preceding table. The following function uses a pattern matching switch expression to express whether a represents a weekend or a weekday: + +```csharp +private static bool IsWeekDay(DateTime timeOfToll) => + timeOfToll.DayOfWeek switch + { + DayOfWeek.Monday => true, + DayOfWeek.Tuesday => true, + DayOfWeek.Wednesday => true, + DayOfWeek.Thursday => true, + DayOfWeek.Friday => true, + DayOfWeek.Saturday => false, + DayOfWeek.Sunday => false + }; +``` + +That method is correct, but it's repetitious. You can simplify it, as shown in the following code: + +:::code language="csharp" source="../snippets/patterns/finished/toll-calculator/TollCalculator.cs" ID="IsWeekDay"::: + +Next, add a similar function to categorize the time into the blocks: + +:::code language="csharp" source="../snippets/patterns/finished/toll-calculator/TollCalculator.cs" ID="GetTimeBand"::: + +You add a private `enum` to convert each range of time to a discrete value. Then, the `GetTimeBand` method uses [relational patterns](../../language-reference/operators/patterns.md#relational-patterns), and [conjunctive `or` patterns](../../language-reference/operators/patterns.md#logical-patterns). A relational pattern lets you test a numeric value using `<`, `>`, `<=`, or `>=`. The `or` pattern tests if an expression matches one or more patterns. You can also use an `and` pattern to ensure that an expression matches two distinct patterns, and a `not` pattern to test that an expression doesn't match a pattern. + +After you create those methods, you can use another `switch` expression with the **tuple pattern** to calculate the pricing premium. You could build a `switch` expression with all 16 arms: + +:::code language="csharp" source="../snippets/patterns/finished/toll-calculator/TollCalculator.cs" ID="TuplePatternOne"::: + +The above code works, but it can be simplified. All eight combinations for the weekend have the same toll. You can replace all eight with the following line: + +```csharp +(false, _, _) => 1.0m, +``` + +Both inbound and outbound traffic have the same multiplier during the weekday daytime and overnight hours. Those four switch arms can be replaced with the following two lines: + +```csharp +(true, TimeBand.Overnight, _) => 0.75m, +(true, TimeBand.Daytime, _) => 1.5m, +``` + +The code should look like the following code after those two changes: + +```csharp +public decimal PeakTimePremium(DateTime timeOfToll, bool inbound) => + (IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch + { + (true, TimeBand.MorningRush, true) => 2.00m, + (true, TimeBand.MorningRush, false) => 1.00m, + (true, TimeBand.Daytime, _) => 1.50m, + (true, TimeBand.EveningRush, true) => 1.00m, + (true, TimeBand.EveningRush, false) => 2.00m, + (true, TimeBand.Overnight, _) => 0.75m, + (false, _, _) => 1.00m, + }; +``` + +Finally, you can remove the two rush hour times that pay the regular price. Once you remove those arms, you can replace the `false` with a discard (`_`) in the final switch arm. You'll have the following finished method: + +:::code language="csharp" source="../snippets/patterns/finished/toll-calculator/TollCalculator.cs" id="FinalTuplePattern"::: + +This example highlights one of the advantages of pattern matching: the pattern branches are evaluated in order. If you rearrange them so that an earlier branch handles one of your later cases, the compiler warns you about the unreachable code. Those language rules made it easier to do the preceding simplifications with confidence that the code didn't change. + +Pattern matching makes some types of code more readable and offers an alternative to object-oriented techniques when you can't add code to your classes. The cloud is causing data and functionality to live apart. The *shape* of the data and the *operations* on it aren't necessarily described together. In this tutorial, you consumed existing data in entirely different ways from its original function. Pattern matching gave you the ability to write functionality that overrode those types, even though you couldn't extend them. + +## Next steps + +You can download the finished code from the [dotnet/samples](https://github.com/dotnet/samples/tree/main/csharp/tutorials/patterns/finished) GitHub repository. Explore patterns on your own and add this technique into your regular coding activities. Learning these techniques gives you another way to approach problems and create new functionality. + +## See also + +- [Patterns](../../language-reference/operators/patterns.md) +- [`switch` expression](../../language-reference/operators/switch-expression.md) diff --git a/docs/csharp/fundamentals/tutorials/pattern-matching.md b/docs/csharp/fundamentals/tutorials/pattern-matching.md index b134a13666f9f..7b2bb412c2f41 100644 --- a/docs/csharp/fundamentals/tutorials/pattern-matching.md +++ b/docs/csharp/fundamentals/tutorials/pattern-matching.md @@ -1,380 +1,136 @@ --- -title: "Tutorial: Build algorithms with pattern matching" -description: This advanced tutorial demonstrates how to use pattern matching techniques to create functionality using data and algorithms that are created separately. -ms.date: 02/25/2022 +title: "Tutorial: Use pattern matching to build object behavior" +description: Build a canal-lock simulation that uses pattern matching to model safe object behavior. +ms.date: 09/29/2026 +ms.topic: tutorial +ai-usage: ai-assisted --- -# Tutorial: Use pattern matching to build type-driven and data-driven algorithms -You can write functionality that behaves as though you extended types that may be in other libraries. Another use for patterns is to create functionality your application requires that isn't a fundamental feature of the type being extended. +# Tutorial: Use pattern matching to build object behavior -In this tutorial, you'll learn how to: - -> [!div class="checklist"] +> [!TIP] +> **New to developing software?** Start with the [Get started](../../tour-of-csharp/tutorials/index.md) tutorials first. They introduce classes, methods, and control flow. > -> - Recognize situations where pattern matching should be used. -> - Use pattern matching expressions to implement behavior based on types and property values. -> - Combine pattern matching with other techniques to create complete algorithms. - -## Prerequisites - -[!INCLUDE [Prerequisites](../../../../includes/prerequisites-basic-winget.md)] - -This tutorial assumes you're familiar with C# and .NET, including either Visual Studio or the .NET CLI. +> **Experienced in another language?** This tutorial shows how C# patterns can express object behavior clearly when rules depend on the current state of an object. -## Scenarios for pattern matching +In this tutorial, you build a console app that models the rules for a canal lock. -Modern development often includes integrating data from multiple sources and presenting information and insights from that data in a single cohesive application. You and your team won't have control or access for all the types that represent the incoming data. +A canal lock raises or lowers boats between two stretches of water at different heights. It has two gates and a chamber whose water level changes between a low setting and a high setting. The lock can operate safely only when the water level and gate positions stay in valid combinations. -The classic object-oriented design would call for creating types in your application that represent each data type from those multiple data sources. Then, your application would work with those new types, build inheritance hierarchies, create virtual methods, and implement abstractions. Those techniques work, and sometimes they're the best tools. Other times you can write less code. You can write more clear code using techniques that separate the data from the operations that manipulate that data. +In this tutorial, you learn how to: -In this tutorial, you'll create and explore an application that takes incoming data from several external sources for a single scenario. You'll see how **pattern matching** provides an efficient way to consume and process that data in ways that weren't part of the original system. +> [!div class="checklist"] +> +> - Express object behavior by matching on state. +> - Implement those rules with C# pattern matching. +> - Use compiler diagnostics to validate your implementation. -Consider a major metropolitan area that is using tolls and peak time pricing to manage traffic. You write an application that calculates tolls for a vehicle based on its type. Later enhancements incorporate pricing based on the number of occupants in the vehicle. Further enhancements add pricing based on the time and the day of the week. +## Prerequisites -From that brief description, you may have quickly sketched out an object hierarchy to model this system. However, your data is coming from multiple sources like other vehicle registration management systems. These systems provide different classes to model that data and you don't have a single object model you can use. In this tutorial, you'll use these simplified classes to model for the vehicle data from these external systems, as shown in the following code: +[!INCLUDE [Prerequisites](../../../../includes/prerequisites-basic-winget.md)] -:::code language="csharp" source="./snippets/patterns/start/toll-calculator/ExternalSystems.cs"::: +## Build a simulation of a canal lock -You can download the starter code from the [dotnet/samples](https://github.com/dotnet/samples/tree/main/csharp/tutorials/patterns/start) GitHub repository. You can see that the vehicle classes are from different systems, and are in different namespaces. No common base class, other than `System.Object` can be used. +A canal lock raises and lowers boats between waterways at different levels. In this tutorial, the simulated lock has a lower gate, an upper gate, and water that can be either low or high. -## Pattern matching designs +In normal operation, a boat enters when the water level inside the lock matches the level on the entry side. Once the boat is inside, both gates close. The water level changes to match the exit side, and then the exit gate opens. To keep the model safe, the water level can change only when both gates are closed, and a gate can open only when the water level matches that side. -The scenario used in this tutorial highlights the kinds of problems that pattern matching is well suited to solve: +You can model those rules with a `CanalLock` class. It exposes commands to open or close either gate and to raise or lower the water. It also exposes properties that report the current state of the lock. -- The objects you need to work with aren't in an object hierarchy that matches your goals. You may be working with classes that are part of unrelated systems. -- The functionality you're adding isn't part of the core abstraction for these classes. The toll paid by a vehicle *changes* for different types of vehicles, but the toll isn't a core function of the vehicle. +## Define the class -When the *shape* of the data and the *operations* on that data aren't described together, the pattern matching features in C# make it easier to work with. +Create a console project, then add a class named `CanalLock`. Start by designing the public API and leaving the methods unimplemented: -## Implement the basic toll calculations +:::code language="csharp" source="./snippets/pattern-matching-objects/InterimSteps.cs" ID="APIDesign"::: -The most basic toll calculation relies only on the vehicle type: +The preceding code initializes the lock with both gates closed and the water level low. Next, add the following code to `Main` to guide your first implementation: -- A `Car` is $2.00. -- A `Taxi` is $3.50. -- A `Bus` is $5.00. -- A `DeliveryTruck` is $10.00 +:::code language="csharp" source="./snippets/pattern-matching-objects/Program.cs" ID="HappyTests"::: -Create a new `TollCalculator` class, and implement pattern matching on the vehicle type to get the toll amount. The following code shows the initial implementation of the `TollCalculator`. +Now add a first implementation that changes each state value without enforcing the safety rules: -```csharp -using System; -using CommercialRegistration; -using ConsumerVehicleRegistration; -using LiveryRegistration; +:::code language="csharp" source="./snippets/pattern-matching-objects/InterimSteps.cs" ID="FirstImplementation"::: -namespace Calculators; +These first checks pass. You have the mechanics working. Next, add a test for the first failure condition. At the end of the previous sequence, both gates are closed and the water level is low. Try to open the upper gate: -public class TollCalculator -{ - public decimal CalculateToll(object vehicle) => - vehicle switch - { - Car c => 2.00m, - Taxi t => 3.50m, - Bus b => 5.00m, - DeliveryTruck t => 10.00m, - { } => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)), - null => throw new ArgumentNullException(nameof(vehicle)) - }; -} -``` +:::code language="csharp" source="./snippets/pattern-matching-objects/Program.cs" ID="HighGateSafetyTest"::: -The preceding code uses a [`switch` expression](../../language-reference/operators/switch-expression.md) (not the same as a [`switch` statement](../../language-reference/statements/selection-statements.md#the-switch-statement)) that tests the [declaration pattern](../../language-reference/operators/patterns.md#declaration-and-type-patterns). A **switch expression** begins with the variable, `vehicle` in the preceding code, followed by the `switch` keyword. Next comes all the **switch arms** inside curly braces. The `switch` expression makes other refinements to the syntax that surrounds the `switch` statement. The `case` keyword is omitted, and the result of each arm is an expression. The last two arms show a new language feature. The `{ }` case matches any non-null object that didn't match an earlier arm. This arm catches any incorrect types passed to this method. The `{ }` case must follow the cases for each vehicle type. If the order were reversed, the `{ }` case would take precedence. Finally, the `null` [constant pattern](../../language-reference/operators/patterns.md#constant-pattern) detects when `null` is passed to this method. The `null` pattern can be last because the other patterns match only a non-null object of the correct type. +That test fails because the upper gate opens when it should not. A first fix could look like this: -You can test this code using the following code in `Program.cs`: +:::code language="csharp" source="./snippets/pattern-matching-objects/InterimSteps.cs" ID="SecondImplementation"::: -```csharp -using System; -using CommercialRegistration; -using ConsumerVehicleRegistration; -using LiveryRegistration; +Your tests pass again. But as you add more conditions, more `if` statements accumulate. The code gets harder to scan because each rule is separated from the others. -using toll_calculator; +## Implement the commands with patterns -var tollCalc = new TollCalculator(); +A clearer option is to use *patterns* to describe the valid combinations directly. In the next step, each switch expression uses one tuple as the *pattern input*. C# evaluates that tuple once, and each switch arm tests the current gate state, the water level, and the requested new setting. -var car = new Car(); -var taxi = new Taxi(); -var bus = new Bus(); -var truck = new DeliveryTruck(); +For the upper gate, those combinations can be summarized like this: -Console.WriteLine($"The toll for a car is {tollCalc.CalculateToll(car)}"); -Console.WriteLine($"The toll for a taxi is {tollCalc.CalculateToll(taxi)}"); -Console.WriteLine($"The toll for a bus is {tollCalc.CalculateToll(bus)}"); -Console.WriteLine($"The toll for a truck is {tollCalc.CalculateToll(truck)}"); +| New setting | Gate state | Water level | Result | +| --- | --- | --- | --- | +| Closed | Closed | High | Closed | +| Closed | Closed | Low | Closed | +| Closed | Open | High | Closed | +| ~~Closed~~ | ~~Open~~ | ~~Low~~ | ~~Closed~~ | +| Open | Closed | High | Open | +| Open | Closed | Low | Closed (error) | +| Open | Open | High | Open | +| ~~Open~~ | ~~Open~~ | ~~Low~~ | ~~Closed (error)~~ | -try -{ - tollCalc.CalculateToll("this will fail"); -} -catch (ArgumentException e) -{ - Console.WriteLine("Caught an argument exception when using the wrong type"); -} -try -{ - tollCalc.CalculateToll(null!); -} -catch (ArgumentNullException e) -{ - Console.WriteLine("Caught an argument exception when using null"); -} -``` +The struck-through rows represent invalid internal states. The switch expression can encode the valid transitions directly. `false` still means the gate is closed: -That code is included in the starter project, but is commented out. Remove the comments, and you can test what you've written. +:::code language="csharp" source="./snippets/pattern-matching-objects/InterimSteps.cs" ID="ThirdImplementation"::: -You're starting to see how patterns can help you create algorithms where the code and the data are separate. The `switch` expression tests the type and produces different values based on the results. That's only the beginning. - -## Add occupancy pricing - -The toll authority wants to encourage vehicles to travel at maximum capacity. They've decided to charge more when vehicles have fewer passengers, and encourage full vehicles by offering lower pricing: - -- Cars and taxis with no passengers pay an extra $0.50. -- Cars and taxis with two passengers get a $0.50 discount. -- Cars and taxis with three or more passengers get a $1.00 discount. -- Buses that are less than 50% full pay an extra $2.00. -- Buses that are more than 90% full get a $1.00 discount. - -These rules can be implemented using a [property pattern](../../language-reference/operators/patterns.md#property-pattern) in the same switch expression. A property pattern compares a property value to a constant value. The property pattern examines properties of the object once the type has been determined. The single case for a `Car` expands to four different cases: +Try this version. Your tests pass. The compiler also warns that the switch expression is not exhaustive because `WaterLevel` is an enum, and the compiler considers every value of its underlying numeric type. Add a final discard arm to handle impossible internal states: ```csharp -vehicle switch -{ - Car {Passengers: 0} => 2.00m + 0.50m, - Car {Passengers: 1} => 2.0m, - Car {Passengers: 2} => 2.0m - 0.50m, - Car => 2.00m - 1.0m, - - // ... -}; +_ => throw new InvalidOperationException("Invalid internal state"), ``` -The first three cases test the type as a `Car`, then check the value of the `Passengers` property. If both match, that expression is evaluated and returned. +That discard arm must be last because it matches every remaining input. -You would also expand the cases for taxis in a similar manner: +You can then simplify the earlier arms. Closing the gate is always allowed, so one arm can replace the four separate closed cases: ```csharp -vehicle switch -{ - // ... - - Taxi {Fares: 0} => 3.50m + 1.00m, - Taxi {Fares: 1} => 3.50m, - Taxi {Fares: 2} => 3.50m - 0.50m, - Taxi => 3.50m - 1.00m, - - // ... -}; +(false, _, _) => false, ``` -Next, implement the occupancy rules by expanding the cases for buses, as shown in the following example: +You can also combine the valid open cases and preserve the one safety error: ```csharp -vehicle switch -{ - // ... - - Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m, - Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m, - Bus => 5.00m, - - // ... -}; +(true, _, WaterLevel.High) => true, +(true, false, WaterLevel.Low) => throw new InvalidOperationException("Cannot open high gate when the water is low"), +_ => throw new InvalidOperationException("Invalid internal state"), ``` -The toll authority isn't concerned with the number of passengers in the delivery trucks. Instead, they adjust the toll amount based on the weight class of the trucks as follows: +Run the program again. The tests still pass. Here is the final `SetHighGate` implementation: -- Trucks over 5000 lbs are charged an extra $5.00. -- Light trucks under 3000 lbs are given a $2.00 discount. +:::code language="csharp" source="./snippets/pattern-matching-objects/CanalLock.cs" ID="FinalImplementation"::: -That rule is implemented with the following code: +## Implement the remaining rules -```csharp -vehicle switch -{ - // ... +Now apply the same idea to `SetLowGate` and `SetWaterLevel`. Start by adding tests that expose invalid operations: - DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m, - DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m, - DeliveryTruck => 10.00m, -}; -``` +:::code language="csharp" source="./snippets/pattern-matching-objects/Program.cs" ID="FinalTestCode"::: -The preceding code shows the `when` clause of a switch arm. You use the `when` clause to test conditions other than equality on a property. When you've finished, you'll have a method that looks much like the following code: +Run the app again. These tests fail, and the canal lock reaches invalid states. Implement the remaining methods by matching on the full state. `SetLowGate` is similar to `SetHighGate`. `SetWaterLevel` uses the current water level plus both gate positions: ```csharp -vehicle switch +CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch { - Car {Passengers: 0} => 2.00m + 0.50m, - Car {Passengers: 1} => 2.0m, - Car {Passengers: 2} => 2.0m - 0.50m, - Car => 2.00m - 1.0m, - - Taxi {Fares: 0} => 3.50m + 1.00m, - Taxi {Fares: 1} => 3.50m, - Taxi {Fares: 2} => 3.50m - 0.50m, - Taxi => 3.50m - 1.00m, - - Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m, - Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m, - Bus => 5.00m, - - DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m, - DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m, - DeliveryTruck => 10.00m, - - { } => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)), - null => throw new ArgumentNullException(nameof(vehicle)) + // arms go here }; ``` -Many of these switch arms are examples of **recursive patterns**. For example, `Car { Passengers: 1}` shows a constant pattern inside a property pattern. - -You can make this code less repetitive by using nested switches. The `Car` and `Taxi` both have four different arms in the preceding examples. In both cases, you can create a declaration pattern that feeds into a constant pattern. This technique is shown in the following code: - -```csharp -public decimal CalculateToll(object vehicle) => - vehicle switch - { - Car c => c.Passengers switch - { - 0 => 2.00m + 0.5m, - 1 => 2.0m, - 2 => 2.0m - 0.5m, - _ => 2.00m - 1.0m - }, - - Taxi t => t.Fares switch - { - 0 => 3.50m + 1.00m, - 1 => 3.50m, - 2 => 3.50m - 0.50m, - _ => 3.50m - 1.00m - }, - - Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m, - Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m, - Bus b => 5.00m, - - DeliveryTruck t when (t.GrossWeightClass > 5000) => 10.00m + 5.00m, - DeliveryTruck t when (t.GrossWeightClass < 3000) => 10.00m - 2.00m, - DeliveryTruck t => 10.00m, - - { } => throw new ArgumentException(message: "Not a known vehicle type", paramName: nameof(vehicle)), - null => throw new ArgumentNullException(nameof(vehicle)) - }; -``` - -In the preceding sample, using a recursive expression means you don't repeat the `Car` and `Taxi` arms containing child arms that test the property value. This technique isn't used for the `Bus` and `DeliveryTruck` arms because those arms are testing ranges for the property, not discrete values. - -## Add peak pricing - -For the final feature, the toll authority wants to add time sensitive peak pricing. During the morning and evening rush hours, the tolls are doubled. That rule only affects traffic in one direction: inbound to the city in the morning, and outbound in the evening rush hour. During other times during the workday, tolls increase by 50%. Late night and early morning, tolls are reduced by 25%. During the weekend, it's the normal rate, regardless of the time. You could use a series of `if` and `else` statements to express this using the following code: - -[!code-csharp[FullTuplePattern](./snippets/patterns/finished/toll-calculator/TollCalculator.cs#SnippetPremiumWithoutPattern)] - -The preceding code does work correctly, but isn't readable. You have to chain through all the input cases and the nested `if` statements to reason about the code. Instead, you'll use pattern matching for this feature, but you'll integrate it with other techniques. You could build a single pattern match expression that would account for all the combinations of direction, day of the week, and time. The result would be a complicated expression. It would be hard to read and difficult to understand. That makes it hard to ensure correctness. Instead, combine those methods to build a tuple of values that concisely describes all those states. Then use pattern matching to calculate a multiplier for the toll. The tuple contains three discrete conditions: - -- The day is either a weekday or a weekend. -- The band of time when the toll is collected. -- The direction is into the city or out of the city - -The following table shows the combinations of input values and the peak pricing multiplier: - -| Day | Time | Direction | Premium | -| ---------- | ------------ | --------- |--------:| -| Weekday | morning rush | inbound | x 2.00 | -| Weekday | morning rush | outbound | x 1.00 | -| Weekday | daytime | inbound | x 1.50 | -| Weekday | daytime | outbound | x 1.50 | -| Weekday | evening rush | inbound | x 1.00 | -| Weekday | evening rush | outbound | x 2.00 | -| Weekday | overnight | inbound | x 0.75 | -| Weekday | overnight | outbound | x 0.75 | -| Weekend | morning rush | inbound | x 1.00 | -| Weekend | morning rush | outbound | x 1.00 | -| Weekend | daytime | inbound | x 1.00 | -| Weekend | daytime | outbound | x 1.00 | -| Weekend | evening rush | inbound | x 1.00 | -| Weekend | evening rush | outbound | x 1.00 | -| Weekend | overnight | inbound | x 1.00 | -| Weekend | overnight | outbound | x 1.00 | - -There are 16 different combinations of the three variables. By combining some of the conditions, you'll simplify the final switch expression. - -The system that collects the tolls uses a structure for the time when the toll was collected. Build member methods that create the variables from the preceding table. The following function uses a pattern matching switch expression to express whether a represents a weekend or a weekday: - -```csharp -private static bool IsWeekDay(DateTime timeOfToll) => - timeOfToll.DayOfWeek switch - { - DayOfWeek.Monday => true, - DayOfWeek.Tuesday => true, - DayOfWeek.Wednesday => true, - DayOfWeek.Thursday => true, - DayOfWeek.Friday => true, - DayOfWeek.Saturday => false, - DayOfWeek.Sunday => false - }; -``` - -That method is correct, but it's repetitious. You can simplify it, as shown in the following code: - -:::code language="csharp" source="./snippets/patterns/finished/toll-calculator/TollCalculator.cs" ID="IsWeekDay"::: - -Next, add a similar function to categorize the time into the blocks: - -:::code language="csharp" source="./snippets/patterns/finished/toll-calculator/TollCalculator.cs" ID="GetTimeBand"::: - -You add a private `enum` to convert each range of time to a discrete value. Then, the `GetTimeBand` method uses [relational patterns](../../language-reference/operators/patterns.md#relational-patterns), and [conjunctive `or` patterns](../../language-reference/operators/patterns.md#logical-patterns). A relational pattern lets you test a numeric value using `<`, `>`, `<=`, or `>=`. The `or` pattern tests if an expression matches one or more patterns. You can also use an `and` pattern to ensure that an expression matches two distinct patterns, and a `not` pattern to test that an expression doesn't match a pattern. - -After you create those methods, you can use another `switch` expression with the **tuple pattern** to calculate the pricing premium. You could build a `switch` expression with all 16 arms: - -:::code language="csharp" source="./snippets/patterns/finished/toll-calculator/TollCalculator.cs" ID="TuplePatternOne"::: - -The above code works, but it can be simplified. All eight combinations for the weekend have the same toll. You can replace all eight with the following line: - -```csharp -(false, _, _) => 1.0m, -``` - -Both inbound and outbound traffic have the same multiplier during the weekday daytime and overnight hours. Those four switch arms can be replaced with the following two lines: - -```csharp -(true, TimeBand.Overnight, _) => 0.75m, -(true, TimeBand.Daytime, _) => 1.5m, -``` - -The code should look like the following code after those two changes: - -```csharp -public decimal PeakTimePremium(DateTime timeOfToll, bool inbound) => - (IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch - { - (true, TimeBand.MorningRush, true) => 2.00m, - (true, TimeBand.MorningRush, false) => 1.00m, - (true, TimeBand.Daytime, _) => 1.50m, - (true, TimeBand.EveningRush, true) => 1.00m, - (true, TimeBand.EveningRush, false) => 2.00m, - (true, TimeBand.Overnight, _) => 0.75m, - (false, _, _) => 1.00m, - }; -``` - -Finally, you can remove the two rush hour times that pay the regular price. Once you remove those arms, you can replace the `false` with a discard (`_`) in the final switch arm. You'll have the following finished method: - -:::code language="csharp" source="./snippets/patterns/finished/toll-calculator/TollCalculator.cs" id="FinalTuplePattern"::: - -This example highlights one of the advantages of pattern matching: the pattern branches are evaluated in order. If you rearrange them so that an earlier branch handles one of your later cases, the compiler warns you about the unreachable code. Those language rules made it easier to do the preceding simplifications with confidence that the code didn't change. +You have 16 combinations to consider. Start with the full table, write the switch arms, run the tests, and then simplify repeated outcomes. -Pattern matching makes some types of code more readable and offers an alternative to object-oriented techniques when you can't add code to your classes. The cloud is causing data and functionality to live apart. The *shape* of the data and the *operations* on it aren't necessarily described together. In this tutorial, you consumed existing data in entirely different ways from its original function. Pattern matching gave you the ability to write functionality that overrode those types, even though you couldn't extend them. +Did you end up with methods similar to these? -## Next steps +:::code language="csharp" source="./snippets/pattern-matching-objects/CanalLock.cs" ID="FinalExercise"::: -You can download the finished code from the [dotnet/samples](https://github.com/dotnet/samples/tree/main/csharp/tutorials/patterns/finished) GitHub repository. Explore patterns on your own and add this technique into your regular coding activities. Learning these techniques gives you another way to approach problems and create new functionality. +Your tests should now pass, and the canal lock should enforce its safety rules. -## See also +## Summary -- [Patterns](../../language-reference/operators/patterns.md) -- [`switch` expression](../../language-reference/operators/switch-expression.md) +In this tutorial, you used pattern matching to express how an object can change from one valid state to another. Patterns kept the allowed transitions together so you could compare them more easily than with a long series of branching statements. This approach works well when behavior depends on the combined shape of several state values. diff --git a/docs/csharp/tutorials/snippets/patterns-objects/CanalLock.cs b/docs/csharp/fundamentals/tutorials/snippets/pattern-matching-objects/CanalLock.cs similarity index 98% rename from docs/csharp/tutorials/snippets/patterns-objects/CanalLock.cs rename to docs/csharp/fundamentals/tutorials/snippets/pattern-matching-objects/CanalLock.cs index bbb506f7b8f21..c24333941336d 100644 --- a/docs/csharp/tutorials/snippets/patterns-objects/CanalLock.cs +++ b/docs/csharp/fundamentals/tutorials/snippets/pattern-matching-objects/CanalLock.cs @@ -1,4 +1,4 @@ -namespace pattern_objects; +namespace pattern_matching_objects; public enum WaterLevel { diff --git a/docs/csharp/tutorials/snippets/patterns-objects/InterimSteps.cs b/docs/csharp/fundamentals/tutorials/snippets/pattern-matching-objects/InterimSteps.cs similarity index 100% rename from docs/csharp/tutorials/snippets/patterns-objects/InterimSteps.cs rename to docs/csharp/fundamentals/tutorials/snippets/pattern-matching-objects/InterimSteps.cs diff --git a/docs/csharp/tutorials/snippets/patterns-objects/Program.cs b/docs/csharp/fundamentals/tutorials/snippets/pattern-matching-objects/Program.cs similarity index 98% rename from docs/csharp/tutorials/snippets/patterns-objects/Program.cs rename to docs/csharp/fundamentals/tutorials/snippets/pattern-matching-objects/Program.cs index ef9ec42478655..de7eb51d639b4 100644 --- a/docs/csharp/tutorials/snippets/patterns-objects/Program.cs +++ b/docs/csharp/fundamentals/tutorials/snippets/pattern-matching-objects/Program.cs @@ -1,4 +1,4 @@ -namespace pattern_objects; +namespace pattern_matching_objects; class Program { diff --git a/docs/csharp/tutorials/snippets/patterns-objects/pattern-objects.csproj b/docs/csharp/fundamentals/tutorials/snippets/pattern-matching-objects/pattern-matching-objects.csproj similarity index 79% rename from docs/csharp/tutorials/snippets/patterns-objects/pattern-objects.csproj rename to docs/csharp/fundamentals/tutorials/snippets/pattern-matching-objects/pattern-matching-objects.csproj index eda50b0eef5c1..7dea585fa89a1 100644 --- a/docs/csharp/tutorials/snippets/patterns-objects/pattern-objects.csproj +++ b/docs/csharp/fundamentals/tutorials/snippets/pattern-matching-objects/pattern-matching-objects.csproj @@ -5,7 +5,7 @@ net8.0 enable enable - pattern_objects + pattern_matching_objects diff --git a/docs/csharp/fundamentals/types/tuples.md b/docs/csharp/fundamentals/types/tuples.md index 39a3fa019d74f..8fb3e2e1137be 100644 --- a/docs/csharp/fundamentals/types/tuples.md +++ b/docs/csharp/fundamentals/types/tuples.md @@ -12,7 +12,7 @@ ai-usage: ai-assisted > > **Experienced in another language?** C# tuples are value types similar to tuples in Python or Swift, but with optional named elements and full deconstruction support. Skim the [deconstruction](#deconstruct-tuples) and [equality](#tuple-equality) sections for C#-specific patterns. -A *tuple* groups multiple values into a single, lightweight structure without requiring you to define a named type. Tuples are value types that you can declare inline, return from methods, and deconstruct into individual variables. Use tuples when you need a quick, temporary grouping of related values. For example, when you return multiple results from a method or store a coordinate pair. +A *tuple* groups multiple values into a single, lightweight structure without requiring you to define a named type. Tuples are value types that you can declare inline, return from methods, and deconstruct into individual variables. Use tuples when you need a quick, temporary grouping of related values. For example, when you return multiple results from a method or store a coordinate pair. The following example creates a tuple with named elements and accesses each element by name: @@ -97,6 +97,6 @@ Tuples are the preferred choice when you need a lightweight unnamed data structu ## See also - [Tuple types (C# reference)](../../language-reference/builtin-types/value-tuples.md) for complete syntax details -- [Deconstructing tuples and other types](../functional/deconstruct.md) for user-defined `Deconstruct` methods +- [Deconstructing tuples and other types](../patterns/deconstruct.md) for user-defined `Deconstruct` methods - [Discards](../patterns/discards.md) - [Records](records.md) diff --git a/docs/csharp/language-reference/builtin-types/value-tuples.md b/docs/csharp/language-reference/builtin-types/value-tuples.md index d1a316d496f26..54b7ff4bd0013 100644 --- a/docs/csharp/language-reference/builtin-types/value-tuples.md +++ b/docs/csharp/language-reference/builtin-types/value-tuples.md @@ -126,7 +126,7 @@ You can also combine deconstruction with [pattern matching](../../fundamentals/p :::code language="csharp" source="snippets/shared/ValueTuples.cs" id="DeconstructToPattern"::: -For more information about deconstruction of tuples and other types, see [Deconstructing tuples and other types](../../fundamentals/functional/deconstruct.md). +For more information about deconstruction of tuples and other types, see [Deconstructing tuples and other types](../../fundamentals/patterns/deconstruct.md). ## Tuple equality diff --git a/docs/csharp/language-reference/compiler-messages/deconstruction-errors.md b/docs/csharp/language-reference/compiler-messages/deconstruction-errors.md index 7b2148a3ebf4f..edbbd429c8190 100644 --- a/docs/csharp/language-reference/compiler-messages/deconstruction-errors.md +++ b/docs/csharp/language-reference/compiler-messages/deconstruction-errors.md @@ -49,7 +49,7 @@ That's by design. The text closely matches the text of the compiler error or war - **CS8129**: *No suitable 'Deconstruct' instance or extension method was found for type 'type', with count out parameters and a void return type.* -Provide an accessible instance or extension `Deconstruct` method that returns `void` and has one `out` parameter for each variable on the left. Match each parameter type to the corresponding deconstruction variable (**CS8129**). For more information, see [user-defined deconstruction](../../fundamentals/functional/deconstruct.md#user-defined-types) and the [`out` parameter modifier](../keywords/method-parameters.md#out-parameter-modifier). +Provide an accessible instance or extension `Deconstruct` method that returns `void` and has one `out` parameter for each variable on the left. Match each parameter type to the corresponding deconstruction variable (**CS8129**). For more information, see [user-defined deconstruction](../../fundamentals/patterns/deconstruct.md#deconstruct-user-defined-types) and the [`out` parameter modifier](../keywords/method-parameters.md#out-parameter-modifier). ## Type inference for deconstruction variables, discards, and `out` variables @@ -58,7 +58,7 @@ Provide an accessible instance or extension `Deconstruct` method that returns `v - **CS8183**: *Cannot infer the type of implicitly-typed discard.* - **CS8197**: *Cannot infer the type of implicitly-typed out variable 'variable'.* -Supply a typed, deconstructable expression on the right so the compiler can determine each implicitly typed variable (**CS8130**, **CS8131**). Cast or otherwise give a discarded expression a type; in a deconstruction, specify an element type when appropriate (**CS8183**). For an `out` variable, use a method parameter that supplies the type or specify the type explicitly in the `out` argument (**CS8197**). For more information, see [deconstruction](../../fundamentals/functional/deconstruct.md) and [calls with `out` parameters](../../fundamentals/patterns/discards.md#calls-to-methods-with-out-parameters). +Supply a typed, deconstructable expression on the right so the compiler can determine each implicitly typed variable (**CS8130**, **CS8131**). Cast or otherwise give a discarded expression a type; in a deconstruction, specify an element type when appropriate (**CS8183**). For an `out` variable, use a method parameter that supplies the type or specify the type explicitly in the `out` argument (**CS8197**). For more information, see [deconstruction](../../fundamentals/patterns/deconstruct.md) and [calls with `out` parameters](../../fundamentals/patterns/discards.md#calls-to-methods-with-out-parameters). ## Deconstruction cardinality @@ -81,4 +81,4 @@ var (x, y) = point; (a, b) = point; ``` -For more information, see [tuple deconstruction](../../fundamentals/functional/deconstruct.md#tuples). +For more information, see [tuple deconstruction](../../fundamentals/patterns/deconstruct.md#deconstruct-tuples). diff --git a/docs/csharp/language-reference/compiler-messages/tuple-errors.md b/docs/csharp/language-reference/compiler-messages/tuple-errors.md index a1169536632e7..d9f1c9bb0c47d 100644 --- a/docs/csharp/language-reference/compiler-messages/tuple-errors.md +++ b/docs/csharp/language-reference/compiler-messages/tuple-errors.md @@ -143,6 +143,6 @@ These errors relate to tuple expression formation. Tuples require at least two e ## See also - [Value tuples](../builtin-types/value-tuples.md) -- [Deconstruction](../../fundamentals/functional/deconstruct.md) +- [Deconstruction](../../fundamentals/patterns/deconstruct.md) - [Pattern matching](../../fundamentals/patterns/pattern-matching.md) - [Void](../builtin-types/void.md) diff --git a/docs/csharp/language-reference/keywords/method-parameters.md b/docs/csharp/language-reference/keywords/method-parameters.md index fb63a847d38cc..fc15aae9bcb15 100644 --- a/docs/csharp/language-reference/keywords/method-parameters.md +++ b/docs/csharp/language-reference/keywords/method-parameters.md @@ -107,7 +107,7 @@ To use an `out` parameter, both the method definition and the calling method mus You don't need to initialize variables passed as `out` arguments before the method call. However, the called method must assign a value before it returns. -[Deconstruct methods](../../fundamentals/functional/deconstruct.md) declare their parameters with the `out` modifier to return multiple values. Other methods can return [value tuples](../builtin-types/value-tuples.md) for multiple return values. +[Deconstruct methods](../../fundamentals/patterns/deconstruct.md) declare their parameters with the `out` modifier to return multiple values. Other methods can return [value tuples](../builtin-types/value-tuples.md) for multiple return values. You can declare a variable in a separate statement before you pass it as an `out` argument. You can also declare the `out` variable in the argument list of the method call, rather than in a separate variable declaration. `out` variable declarations produce more compact, readable code, and also prevent you from inadvertently assigning a value to the variable before the method call. The following example defines the `number` variable in the call to the [Int32.TryParse](xref:System.Int32.TryParse(System.String,System.Int32@)) method. diff --git a/docs/csharp/language-reference/operators/is.md b/docs/csharp/language-reference/operators/is.md index 2ec9dbe54a6a4..cefd416115e0a 100644 --- a/docs/csharp/language-reference/operators/is.md +++ b/docs/csharp/language-reference/operators/is.md @@ -51,5 +51,5 @@ For more information, see [The is operator](~/_csharpstandard/standard/expressio - [C# operators and expressions](index.md) - [Patterns](patterns.md) -- [Tutorial: Use pattern matching to build type-driven and data-driven algorithms](../../fundamentals/tutorials/pattern-matching.md) +- [Tutorial: Use pattern matching to build type-driven and data-driven algorithms](../../fundamentals/tutorials/advanced/pattern-matching.md) - [Type-testing and cast operators](../operators/type-testing-and-cast.md) diff --git a/docs/csharp/language-reference/operators/patterns.md b/docs/csharp/language-reference/operators/patterns.md index 2159402a79371..07d4a475cb6fa 100644 --- a/docs/csharp/language-reference/operators/patterns.md +++ b/docs/csharp/language-reference/operators/patterns.md @@ -39,7 +39,7 @@ In those constructs, you can match an input expression against any of the follow [Logical](#logical-patterns), [property](#property-pattern), [positional](#positional-pattern), and [list](#list-patterns) patterns are *recursive* patterns. That is, they can contain *nested* patterns. -For an example of how to use those patterns to build a data-driven algorithm, see [Tutorial: Use pattern matching to build type-driven and data-driven algorithms](../../fundamentals/tutorials/pattern-matching.md). +For an example of how to use those patterns to build a data-driven algorithm, see [Tutorial: Use pattern matching to build type-driven and data-driven algorithms](../../fundamentals/tutorials/advanced/pattern-matching.md). ## Declaration and type patterns @@ -209,7 +209,7 @@ Use a *positional pattern* to deconstruct an expression and match the resulting :::code language="csharp" source="snippets/patterns/PositionalPattern.cs" id="BasicExample"::: -In the preceding example, the type of an expression contains the [Deconstruct](../../fundamentals/functional/deconstruct.md) method, which the pattern uses to deconstruct an expression result. +In the preceding example, the type of an expression contains the [Deconstruct](../../fundamentals/patterns/deconstruct.md) method, which the pattern uses to deconstruct an expression result. >[!IMPORTANT] > The order of members in a positional pattern must match the order of parameters in the `Deconstruct` method. The code generated for the positional pattern calls the `Deconstruct` method. @@ -401,4 +401,4 @@ For more information, see the [Patterns and pattern matching](~/_csharpstandard/ - [C# operators and expressions](index.md) - [Pattern matching overview](../../fundamentals/patterns/pattern-matching.md) -- [Tutorial: Use pattern matching to build type-driven and data-driven algorithms](../../fundamentals/tutorials/pattern-matching.md) +- [Tutorial: Use pattern matching to build type-driven and data-driven algorithms](../../fundamentals/tutorials/advanced/pattern-matching.md) diff --git a/docs/csharp/language-reference/operators/switch-expression.md b/docs/csharp/language-reference/operators/switch-expression.md index 00f9c9ea2c03a..5049c69b603ed 100644 --- a/docs/csharp/language-reference/operators/switch-expression.md +++ b/docs/csharp/language-reference/operators/switch-expression.md @@ -62,5 +62,5 @@ For more information, see the [`switch` expression](~/_csharpstandard/standard/e - [Add missing cases to switch expression (style rule IDE0072)](../../../fundamentals/code-analysis/style-rules/ide0072.md) - [C# operators and expressions](index.md) - [Patterns](patterns.md) -- [Tutorial: Use pattern matching to build type-driven and data-driven algorithms](../../fundamentals/tutorials/pattern-matching.md) +- [Tutorial: Use pattern matching to build type-driven and data-driven algorithms](../../fundamentals/tutorials/advanced/pattern-matching.md) - [`switch` statement](../statements/selection-statements.md#the-switch-statement) diff --git a/docs/csharp/misc/cs0819.md b/docs/csharp/misc/cs0819.md index a77555f8afbc4..b38bc99de64d5 100644 --- a/docs/csharp/misc/cs0819.md +++ b/docs/csharp/misc/cs0819.md @@ -20,7 +20,7 @@ There are three options: 1. If the variables are of the same type, use explicit declarations. 1. Declare and assign a value to each implicitly typed local variable on a separate line. -1. Declare a variable using [Tuple deconstruction](../fundamentals/functional/deconstruct.md#tuples) syntax. **Note**: this option will not work inside a `using` statement as `Tuple` does not implement `IDisposable`. +1. Declare a variable using [Tuple deconstruction](../fundamentals/patterns/deconstruct.md#deconstruct-tuples) syntax. **Note**: this option will not work inside a `using` statement as `Tuple` does not implement `IDisposable`. ## Example 1 @@ -82,4 +82,4 @@ class Program ## See also - [Implicitly Typed Local Variables](../programming-guide/classes-and-structs/implicitly-typed-local-variables.md) -- [Deconstructing Tuples and Other Types](../fundamentals/functional/deconstruct.md#tuples) +- [Deconstructing Tuples and Other Types](../fundamentals/patterns/deconstruct.md#deconstruct-tuples) diff --git a/docs/csharp/toc.yml b/docs/csharp/toc.yml index 90ff633bc2c40..8682adac64b6b 100644 --- a/docs/csharp/toc.yml +++ b/docs/csharp/toc.yml @@ -123,6 +123,8 @@ items: href: fundamentals/patterns/type-patterns.md - name: Property and positional patterns href: fundamentals/patterns/property-positional-patterns.md + - name: Deconstructing tuples and other types + href: fundamentals/patterns/deconstruct.md - name: Relational, logical, and parenthesized patterns href: fundamentals/patterns/relational-logical-patterns.md - name: List and slice patterns @@ -159,19 +161,6 @@ items: # Encapsulation & composition - name: Polymorphism href: fundamentals/object-oriented/polymorphism.md - - name: Functional techniques - items: - # - Conceptual overview - # - Expressions (how different than OO and expressions?) - # - separation of data & algorithms - # - immutability (emphasize constructors & passing data to constructor, get/init) - # - Functional purity / functions as arguments or return values - # In F#, discussion of "object programming" -> objects become more sophisticated records. - # - language features: - # - Lambdas - # - Positional records as DTOs - - name: Deconstructing tuples and other types - href: fundamentals/functional/deconstruct.md - name: Exceptions and errors items: - name: Overview @@ -221,8 +210,10 @@ items: href: fundamentals/tutorials/safely-cast-using-pattern-matching-is-and-as-operators.md - name: Choosing between tuples, records, structs, and classes href: fundamentals/tutorials/choosing-types.md - - name: Build data-driven algorithms with pattern matching + - name: Build object behavior with pattern matching href: fundamentals/tutorials/pattern-matching.md + - name: Build data-driven algorithms with pattern matching + href: fundamentals/tutorials/advanced/pattern-matching.md - name: Use record types href: fundamentals/tutorials/records.md # separating data and algorithms @@ -280,8 +271,6 @@ items: href: tutorials/top-level-statements.md - name: Explore indexes and ranges href: tutorials/ranges-indexes.md - - name: Explore patterns in objects - href: tutorials/patterns-objects.md - name: Console Application href: tutorials/console-teleprompter.md - name: REST Client diff --git a/docs/csharp/tour-of-csharp/tutorials/pattern-matching.md b/docs/csharp/tour-of-csharp/tutorials/pattern-matching.md index 21c50932bcca6..a2de482225c78 100644 --- a/docs/csharp/tour-of-csharp/tutorials/pattern-matching.md +++ b/docs/csharp/tour-of-csharp/tutorials/pattern-matching.md @@ -142,8 +142,8 @@ To finish this tutorial, explore one more building block for pattern matching: t 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: - [Pattern matching in C#](../../fundamentals/patterns/pattern-matching.md) -- [Explore pattern matching tutorial](../../tutorials/patterns-objects.md) -- [Pattern matching scenario](../../fundamentals/tutorials/pattern-matching.md) +- [Explore pattern matching tutorial](../../fundamentals/tutorials/pattern-matching.md) +- [Pattern matching scenario](../../fundamentals/tutorials/advanced/pattern-matching.md) - [The C# type system](../../fundamentals/types/index.md) — Understand the types you matched against in this tutorial. ## Cleanup resources diff --git a/docs/csharp/tutorials/patterns-objects.md b/docs/csharp/tutorials/patterns-objects.md deleted file mode 100644 index b6b81678b6108..0000000000000 --- a/docs/csharp/tutorials/patterns-objects.md +++ /dev/null @@ -1,126 +0,0 @@ ---- -title: Use patterns in objects C# tutorial -description: This tutorial teaches you techniques to use pattern matching in class members to create better models for object behavior -ms.date: 11/22/2024 ---- -# Use pattern matching to build your class behavior for better code - -The pattern matching features in C# provide syntax to express your algorithms. You can use these techniques to implement the behavior in your classes. You can combine object-oriented class design with a data-oriented implementation to provide concise code while modeling real-world objects. - -In this tutorial, you learn how to: - -> [!div class="checklist"] -> -> - Express your object oriented classes using data patterns. -> - Implement those patterns using C#'s pattern matching features. -> - Leverage compiler diagnostics to validate your implementation. - -## Prerequisites - -[!INCLUDE [Prerequisites](../../../includes/prerequisites-basic.md)] - -## Build a simulation of a canal lock - -In this tutorial, you build a C# class that simulates a [canal lock](https://en.wikipedia.org/wiki/Lock_(water_navigation)). Briefly, a canal lock is a device that raises and lowers boats as they travel between two stretches of water at different levels. A lock has two gates and some mechanism to change the water level. - -In its normal operation, a boat enters one of the gates while the water level in the lock matches the water level on the side the boat enters. Once in the lock, the water level is changed to match the water level where the boat leaves the lock. Once the water level matches that side, the gate on the exit side opens. Safety measures make sure an operator can't create a dangerous situation in the canal. The water level can be changed only when both gates are closed. At most one gate can be open. To open a gate, the water level in the lock must match the water level outside the gate being opened. - -You can build a C# class to model this behavior. A `CanalLock` class would support commands to open or close either gate. It would have other commands to raise or lower the water. The class should also support properties to read the current state of both gates and the water level. Your methods implement the safety measures. - -## Define a class - -You build a console application to test your `CanalLock` class. Create a new console project for .NET 5 using either Visual Studio or the .NET CLI. Then, add a new class and name it `CanalLock`. Next, design your public API, but leave the methods not implemented: - -:::code language="csharp" source="./snippets/patterns-objects/InterimSteps.cs" ID="APIDesign"::: - -The preceding code initializes the object so both gates are closed, and the water level is low. Next, write the following test code in your `Main` method to guide you as you create a first implementation of the class: - -:::code language="csharp" source="snippets/patterns-objects/Program.cs" ID="HappyTests"::: - -Next, add a first implementation of each method in the `CanalLock` class. The following code implements the methods of the class without concern to the safety rules. You add safety tests later: - -:::code language="csharp" source="snippets/patterns-objects/InterimSteps.cs" ID="FirstImplementation"::: - -The tests you wrote so far pass. You implemented the basics. Now, write a test for the first failure condition. At the end of the previous tests, both gates are closed, and the water level is set to low. Add a test to try opening the upper gate: - -:::code language="csharp" source="snippets/patterns-objects/Program.cs" ID="HighGateSafetyTest"::: - -This test fails because the gate opens. As a first implementation, you could fix it with the following code: - -:::code language="csharp" source="snippets/patterns-objects/InterimSteps.cs" ID="SecondImplementation"::: - -Your tests pass. But, as you add more tests, you add more `if` clauses and test different properties. Soon, these methods get too complicated as you add more conditionals. - -## Implement the commands with patterns - -A better way is to use *patterns* to determine if the object is in a valid state to execute a command. You can express if a command is allowed as a function of three variables: the state of the gate, the level of the water, and the new setting: - -| New setting | Gate state | Water Level | Result | -| ----------- | ---------- | ----------- | ------------------ | -| Closed | Closed | High | Closed | -| Closed | Closed | Low | Closed | -| Closed | Open | High | Closed | -| ~~Closed~~ | ~~Open~~ | ~~Low~~ | ~~Closed~~ | -| Open | Closed | High | Open | -| Open | Closed | Low | Closed (Error) | -| Open | Open | High | Open | -| ~~Open~~ | ~~Open~~ | ~~Low~~ | ~~Closed (Error)~~ | - -The fourth and last rows in the table have strike through text because they're invalid. The code you're adding now should make sure the high water gate is never opened when the water is low. Those states can be coded as a single switch expression (remember that `false` indicates "Closed"): - -:::code language="csharp" source="snippets/patterns-objects/InterimSteps.cs" ID="ThirdImplementation"::: - -Try this version. Your tests pass, validating the code. The full table shows the possible combinations of inputs and results. That means you and other developers can quickly look at the table and see that you covered all the possible inputs. Even easier, the compiler can help as well. After you add the previous code, you can see that the compiler generates a warning: *CS8524* indicates the switch expression doesn't cover all possible inputs. The reason for that warning is that one of the inputs is an `enum` type. The compiler interprets "all possible inputs" as all inputs from the underlying type, typically an `int`. This `switch` expression only checks the values declared in the `enum`. To remove the warning, you can add a catch-all discard pattern for the last arm of the expression. This condition throws an exception, because it indicates invalid input: - -```csharp -_ => throw new InvalidOperationException("Invalid internal state"), -``` - -The preceding switch arm must be last in your `switch` expression because it matches all inputs. Experiment by moving it earlier in the order. That causes a compiler error *CS8510* for unreachable code in a pattern. The natural structure of switch expressions enables the compiler to generate errors and warnings for possible mistakes. The compiler "safety net" makes it easier for you to create correct code in fewer iterations, and the freedom to combine switch arms with wildcards. The compiler issues errors if your combination results in unreachable arms you didn't expect, and warnings if you remove a needed arm. - -The first change is to combine all the arms where the command is to close the gate; that's always allowed. Add the following code as the first arm in your switch expression: - -```csharp -(false, _, _) => false, -``` - -After you add the previous switch arm, you'll get four compiler errors, one on each of the arms where the command is `false`. Those arms are already covered by the newly added arm. You can safely remove those four lines. You intended this new switch arm to replace those conditions. - -Next, you can simplify the four arms where the command is to open the gate. In both cases where the water level is high, the gate can be opened. (In one, it's already open.) One case where the water level is low throws an exception, and the other shouldn't happen. It should be safe to throw the same exception if the water lock is already in an invalid state. You can make the following simplifications for those arms: - -```csharp -(true, _, WaterLevel.High) => true, -(true, false, WaterLevel.Low) => throw new InvalidOperationException("Cannot open high gate when the water is low"), -_ => throw new InvalidOperationException("Invalid internal state"), -``` - -Run your tests again, and they pass. Here's the final version of the `SetHighGate` method: - -:::code language="csharp" source="snippets/patterns-objects/CanalLock.cs" ID="FinalImplementation"::: - -## Implement patterns yourself - -Now that you've seen the technique, fill in the `SetLowGate` and `SetWaterLevel` methods yourself. Start by adding the following code to test invalid operations on those methods: - -:::code language="csharp" source="snippets/patterns-objects/Program.cs" ID="FinalTestCode"::: - -Run your application again. You can see the new tests fail, and the canal lock gets into an invalid state. Try to implement the remaining methods yourself. The method to set the lower gate should be similar to the method to set the upper gate. The method that changes the water level has different checks, but should follow a similar structure. You might find it helpful to use the same process for the method that sets the water level. Start with all four inputs: The state of both gates, the current state of the water level, and the requested new water level. The switch expression should start with: - -```csharp -CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch -{ - // elided -}; -``` - -You have 16 total switch arms to fill in. Then, test and simplify. - -Did you make methods something like this? - -:::code language="csharp" source="snippets/patterns-objects/CanalLock.cs" ID="FinalExercise"::: - -Your tests should pass, and the canal lock should operate safely. - -## Summary - -In this tutorial, you learned to use pattern matching to check the internal state of an object before applying any changes to that state. You can check combinations of properties. Once you built tables for any of those transitions, you test your code, then simplify for readability and maintainability. These initial refactorings might suggest further refactorings that validate internal state or manage other API changes. This tutorial combined classes and objects with a more data-oriented, pattern-based approach to implement those classes.