Chapter 12. Deconstruction and pattern matching

This chapter covers

  • Deconstructing tuples into multiple variables
  • Deconstructing nontuple types
  • Applying pattern matching in C# 7
  • Using the three kinds of patterns introduced in C# 7

In chapter 11, you learned that tuples allow you to compose data simply without having to create new types and allowing one variable to act as a bag of other variables. When you used the tuples—for example, to print out the minimum value from a sequence of integers and then print out the maximum—you extracted the values from the tuple one at a time.

That certainly works, and in many cases it’s all you need. But in plenty of cases, you’ll want to break a composite value into separate variables. This operation is called deconstruction. That composite value may be a tuple, or it could be of another type—KeyValuePair, for example. C# 7 provides simple syntax to allow multiple variables to be declared or initialized in a single statement.

Deconstruction occurs in an unconditional way just like a sequence of assignments. Pattern matching is similar, but in a more dynamic context; the input value has to match the pattern in order to execute the code that follows it. C# 7 introduces pattern matching in a couple of contexts and a few kinds of patterns, and there will likely be more in future releases. We’ll start building on chapter 11 by deconstructing the tuples you’ve just created.

12.1. Deconstruction of tuples

C# 7 provides two flavors of deconstruction: one for tuples and one for everything else. They follow the same syntax and have the same general features, but talking about them in the abstract can be confusing. We’ll look at tuples first, and I’ll call out anything that’s tuple specific. In section 12.2, you’ll see how the same ideas are applied to other types. Just to give you an idea of what’s coming, the following listing shows several features of deconstruction, each of which you’ll examine in more detail.

Listing 12.1. Overview of deconstruction using tuples
var tuple = (10, "text");                 1

var (a, b) = tuple;                       2

(int c, string d) = tuple;                3

int e;                                    4
string f;                                 4
(e, f) = tuple;                           4

Console.WriteLine($"a: {a}; b: {b}");     5
Console.WriteLine($"c: {c}; d: {d}");     5
Console.WriteLine($"e: {e}; f: {f}");     5

  • 1 Creates a tuple of type (int, string)
  • 2 Deconstructs to new variables a, b implicitly
  • 3 Deconstructs to new variables c, d explicitly
  • 4 Deconstructs to existing variables
  • 5 Proves that deconstruction works

I suspect that if you were shown that code and told that it would compile, you’d already be able to guess the output, even if you hadn’t read anything about tuples or deconstruction before:

a: 10; b: text
c: 10; d: text
e: 10; f: text

All you’ve done is declared and initialized the six variables a, b, c, d, e, and f in a new way that takes less code than it would’ve before. This isn’t to diminish the usefulness of the feature, but this time there’s relatively little subtlety to go into. In all cases, the operation is as simple as copying a value out of the tuple into a variable. It doesn’t associate the variable with the tuple; changing the variable later won’t change the tuple, or vice versa.

Tuple declaration and deconstruction syntax

The language specification regards deconstruction as closely related to other tuple features. Deconstruction syntax is described in terms of a tuple expression even when you’re not deconstructing tuples (which you’ll see in section 12.2). You probably don’t need to worry too much about that, but you should be aware of potential causes for confusion. Consider these two statements:

(int c, string d) = tuple;
(int c, string d) x = tuple;

The first uses deconstruction to declare two variables (c and d); the second is a declaration of a single variable (x) of tuple type (int c, string d). I don’t think this similarity was a design mistake, but it can take a little getting used to just like expression-bodied members looking like lambda expressions.

Let’s start by looking in more detail at the first two parts of the example, where you declare and initialize in one statement.

12.1.1. Deconstruction to new variables

It’s always been feasible to declare multiple variables in a single statement, but only if they were of the same type. I’ve typically stuck to a single declaration per statement for the sake of readability. But when you can declare and initialize multiple variables in a single statement, and the initial values all have the same source, that’s neat. In particular, if that source is the result of a function call, you can avoid declaring an extra variable just to avoid making multiple calls.

The syntax that’s probably simplest to understand is the one in which each variable is explicitly typed—the same syntax as for a parameter list or tuple type. To clarify my preceding point about the extra variable, the following listing shows a tuple as a result of a method call being deconstructed into three new variables.

Listing 12.2. Calling a method and deconstructing the result into three variables
static (int x, int y, string text) MethodReturningTuple() => (1, 2, "t");

static void Main()
{
    (int a, int b, string name) = MethodReturningTuple();
    Console.WriteLine($"a: {a}; b: {b}; name: {name}");
}

The benefit isn’t as obvious until you consider the equivalent code without using deconstruction. This is what the compiler is transforming the preceding code into:

static void Main()
{
    var tmp = MethodReturningTuple();
    int a = tmp.x;
    int b = tmp.y;
    string name = tmp.text;

    Console.WriteLine($"a: {a}; b: {b}; name: {name}");
}

The three declaration statements don’t bother me too much, although I do appreciate the brevity of the original code, but the tmp variable really niggles. As its name suggests, it’s there only temporarily; its sole purpose is to remember the result of the method call so it can be used to initialize the three variables you really want: a, b, and name. Even though you want tmp only for that bit of code, it has the same scope as the other variables, which feels messy to me. If you want to use implicit typing for some variables but explicit typing for others, that’s fine too, as shown in figure 12.1.

Figure 12.1. Mixing implicit and explicit typing in deconstruction

This is particularly useful if you want to specify a different type than the element type in the original tuple using an implicit conversion for elements where required; see figure 12.2.

Figure 12.2. Deconstruction involving implicit conversions

If you’re happy to use implicit typing for all the variables, C# 7 has shorthand to make it simple; just use var before the list of names:

var (a, b, name) = MethodReturningTuple();

This is equivalent to using var for each variable inside the parameter list, and that in turn is equivalent to explicitly specifying the inferred type based on the type of the value being assigned. Just as with regular implicitly typed variable declarations, using var doesn’t make your code dynamically typed; it just makes the compiler infer the type.

Although you can mix and match between implicit typing and explicit typing in terms of the types specified within the brackets, you can’t use var before the variable list and then provide types for some variables:

var (a, long b, name) = MethodReturningTuple();     1

  • 1 Invalid: mixture of “inside and outside” declarations
A special identifier: _ discards

C# 7 has three features that allow new places to introduce local variables:

In all these cases, specifying a variable name of _ (a single underscore) has a special meaning. It’s a discard, which means “I don’t care about the result. I don’t even want it as a variable at all—just get rid of it.” When a discard is used, it doesn’t introduce a new variable into scope. You can use multiple discards instead of specifying different variable names for multiple variables you don’t care about.

Here’s an example of discards in tuple deconstruction:

var tuple = (1, 2, 3, 4);        1
var (x, y, _, _) = tuple;        2
Console.WriteLine(_);            3

  • 1 Tuple with four elements
  • 2 Deconstructs the tuple but keeps only the first two elements
  • 3 Error CS0103: The name ’_’ doesn’t exist in the current context

If you already have a variable called _ in scope (declared with a regular variable declaration), you can still use discards in deconstruction to an otherwise new set of variables, and the existing variable will remain untouched.

As you saw in our original overview, you don’t have to declare new variables to use deconstruction. Deconstruction can act as a sequence of assignments instead.

12.1.2. Deconstruction assignments to existing variables and properties

The previous section explained most of our original overview example. In this section, we’ll look at this part of the code instead:

var tuple = (10, "text");
int e;
string f;
(e, f) = tuple;

In this case, the compiler isn’t treating the deconstruction as a sequence of declarations with corresponding initialization expressions; instead, it’s just a sequence of assignments. This has the same benefit in terms of avoiding temporary variables that you saw in the previous section. The following listing gives an example using the same MethodReturningTuple() that you used before.

Listing 12.3. Assignments to existing variables using deconstruction
static (int x, int y, string text) MethodReturningTuple() => (1, 2, "t");

static void Main()
{
    int a = 20;                                           1
    int b = 30;                                           1
    string name = "before";                               1
    Console.WriteLine($"a: {a}; b: {b}; name: {name}");   1

    (a, b, name) = MethodReturningTuple();                2

    Console.WriteLine($"a: {a}; b: {b}; name: {name}");   3
}

  • 1 Declares, initializes, and uses three variables
  • 2 Assigns to all three variables using deconstruction
  • 3 Displays the new values

So far, so good, but the feature doesn’t stop with the ability to assign to local variables. Any assignment that would be valid as a separate statement is also valid using deconstruction. That can be an assignment to a field, a property, or an indexer, including working on arrays and other objects.

Declarations or assignments: Not a mixture

Deconstruction allows you to either declare and initialize variables or execute a sequence of assignments. You can’t mix the two. For example, this is invalid:

int x;
(x, int y) = (1, 2);

It’s fine for the assignments to use a variety of targets, however: some existing local variables, some fields, some properties, and so on.

In addition to regular assignments, you can assign to a discard (the _ identifier), thereby effectively throwing away the value if there’s nothing called _ in scope. If you do have a variable named _ in scope, deconstruction assigns to it as normal.

Using _ in deconstruction: Assign or discard?

This looks a little confusing at first: sometimes deconstruction to _ when there’s an existing variable with that name changes the value, and sometimes it discards it. You can avoid this confusion in two ways. The first is to look at the rest of the deconstruction to see whether it’s introducing new variables (in which case _ is a discard) or assigning values to existing variables (in which case _ is assigned a new value like the other variables).

The second way to avoid confusion is to not use _ as a local variable name.

In practice, I expect almost all uses of assignment deconstruction to target either local variables or fields and properties of this. In fact, there’s a neat little technique you can use in constructors that makes the expression-bodied constructors introduced in C# 7 even more useful. Many constructors assign values to properties or fields based on the constructor parameters. You can perform all those assignments in a single expression if you collect the parameters into a tuple literal first, as shown in the next listing.

Listing 12.4. Simple constructor assignments using deconstruction and a tuple literal
public sealed class Point
{
    public double X { get; }
    public double Y { get; }

    public Point(double x, double y) => (X, Y) = (x, y);
}

I really like the brevity of this. I love the clarity of the mapping from constructor parameter to property. The C# compiler even recognizes it as a pattern and avoids constructing a ValueTuple<double, double>. Unfortunately, it still requires a dependency on System.ValueTuple.dll to build, which is enough to put me off using it unless I’m also using tuples somewhere else in the project or targeting a framework that already includes System.ValueTuple.

Is this idiomatic C#?

As I’ve described, this trick has pros and cons. It’s a pure implementation detail of the constructor; it doesn’t even affect the rest of the class body. If you decide to embrace this style and then decide you don’t like it, removing it should be trivial. It’s too early to say whether this will catch on, but I hope so. I’d be wary as soon as the tuple literal needs to be more than just the exact parameter values, though. Even adding a single precondition tips the balance in favor of a regular sequence of assignments, in my subjective opinion.

Assignment deconstruction has an extra wrinkle compared with declaration deconstruction in terms of ordering. Deconstruction that uses assignment has three distinct stages:

  1. Evaluating the targets of the assignments
  2. Evaluating the right-hand side of the assignment operator
  3. Performing the assignments

Those three stages are performed in exactly that order. Within each stage, evaluation occurs in left-to-right source order, as normal. It’s rare that this can make a difference, but it’s possible.

Tip

If you have to worry about this section in order to understand code in front of you, that’s a strong code smell. When you do understand it, I urge you to refactor it. Deconstruction has all the same caveats of using side effects within an expression but amplified because you have multiple evaluations to perform in each stage.

I’m not going to linger on this topic for long; a single example is enough to show the kind of problem you might see. This is by no means the worst example you might find, however. There are all kinds of things you could do in order to make this more convoluted. The following listing deconstructs a (StringBuilder, int) tuple into an existing StringBuilder variable and the Length property associated with that variable.

Listing 12.5. Deconstruction in which evaluation order matters
StringBuilder builder = new StringBuilder("12345");
StringBuilder original = builder;                   1

(builder, builder.Length) =                         2
    (new StringBuilder("67890"), 3);                2

Console.WriteLine(original);                        3
Console.WriteLine(builder);                         3

  • 1 Keeps a reference to original builder for diagnostic reasons
  • 2 Performs the deconstruction assignments
  • 3 Displays the contents of the old and new builders

The middle line is the tricky one here. The key question to consider is which StringBuilder has its Length property set: the one that builder refers to originally or the new value assigned in the first part of the deconstruction? As I described earlier, all the targets for the assignments are evaluated first, before any assignments are performed. The following listing demonstrates this in a sort of exploded version of the same code in which the deconstruction is performed manually.

Listing 12.6. Slow-motion deconstruction to show evaluation order
StringBuilder builder = new StringBuilder("12345");
StringBuilder original = builder;

StringBuilder targetForLength = builder;    1

(StringBuilder, int) tuple =                2
    (new StringBuilder("67890"), 3);        2

builder = tuple.Item1;                      3
targetForLength.Length = tuple.Item2;       3

Console.WriteLine(original);
Console.WriteLine(builder);

  • 1 Evaluates assignment targets
  • 2 Evaluates the tuple literal
  • 3 Performs the assignments on the targets

No extra evaluation is required when the target is just a local variable; you can assign directly to it. But assigning to a property of a variable requires evaluating that variable value as part of the first phase; that’s why you have the targetForLength variable.

After the tuple has been constructed from the literal, you can assign the different items to your targets, making sure you use targetForLength rather than builder when assigning the Length property. The Length property is set on the original StringBuilder with content 12345 rather than the new one with content 67890. That means the output of listings 12.5 and 12.6 is as follows:

123
67890

With that out of the way, there’s one final—and rather more pleasant—wrinkle of tuple construction to talk about before moving on to nontuple deconstruction.

12.1.3. Details of tuple literal deconstruction

As I described in section 11.3.1, not all tuple literals have a type. For example, the tuple literal (null, x => x * 2) doesn’t have a type because neither of its element expressions has a type. But you know it can be converted to type (string, Func<int, int>) because each expression has a conversion to the corresponding type.

The good news is that tuple deconstruction has exactly the same sort of “per element assignment compatibility” as well. This works for both declaration deconstructions and assignment deconstructions. Here’s a brief example:

(string text, Func<int, int> func) =
    (null, x => x * 2);                    1
(text, func) = ("text", x => x * 3);       2

  • 1 Deconstruction declaring text and func
  • 2 Deconstruction assigning to text and func

This also works with deconstruction that requires an implicit conversion from an expression to the target type. For example, using our favorite “int constant within range of byte” example, the following is valid:

(byte x, byte y) = (5, 10);

Like many good language features, this is probably something you might have implicitly expected, but the language needs to be carefully designed and specified to allow it. Now that you’ve looked at tuple deconstruction fairly extensively, deconstruction of nontuples is relatively straightforward.

12.2. Deconstruction of nontuple types

Deconstruction for nontuple types uses a pattern-based[1] approach in the same way async/await does and foreach can. Just as any type with a suitable GetAwaiter method or extension method can be awaited, any type with a suitable Deconstruct method or extension method can be deconstructed using the same syntax as tuples. Let’s start with deconstruction using regular instance methods.

1

This is entirely distinct from the patterns coming up in section 12.3. Apologies for the terminology collision.

12.2.1. Instance deconstruction methods

It’s simplest to demonstrate deconstruction with the Point class used in several examples now. You can add a Deconstruct method to it like this:

public void Deconstruct(out double x, out double y)
{
    x = X;
    y = Y;
}

Then you can deconstruct any Point to two double variables as in the following listing.

Listing 12.7. Deconstructing a Point to two variables
var point = new Point(1.5, 20);   1
var (x, y) = point;               2
Console.WriteLine($"x = {x}");    3
Console.WriteLine($"y = {y}");    3

  • 1 Constructs an instance of point
  • 2 Deconstructs it to two variables of type double
  • 3 Displays the two variable values

The Deconstruct method’s job is to populate the out parameters with the result of the deconstruction. In this case, you’re just deconstructing to two double values. It’s like a constructor in reverse, as the name suggests.

But wait; you used a neat trick with tuples to assign parameter values to properties in the constructor in a single statement. Can you do that here? Yes, you can, and personally, I love it. Here are both the constructor and the Deconstruct method so you can see the similarities:

public Point(double x, double y) => (X, Y) = (x, y);
public void Deconstruct(out double x, out double y) => (x, y) = (X, Y);

The simplicity of this is beautiful, at least after you’ve gotten used to it.

The rules of Deconstruct instance methods used for deconstruction are pretty simple:

  • The method must be accessible to the code doing the deconstruction. (For example, if everything is in the same assembly, it’s fine for Deconstruct to be an internal method.)
  • It must be a void method.
  • There must be at least two parameters. (You can’t deconstruct to a single value.)
  • It must be nongeneric.

You may be wondering why the design uses out parameters instead of requiring that Deconstruct is parameterless but has a tuple return type. The answer is that it’s useful to be able to deconstruct to multiple sets of values, which is feasible with multiple methods, but you can’t overload methods just on return type. To make this clearer, I’ll use an example deconstructing DateTime, but of course, you can’t add your own instance methods to DateTime. It’s time to introduce extension deconstruction methods.

12.2.2. Extension deconstruction methods and overloading

As I briefly stated in the introduction, the compiler finds any Deconstruct methods that follow the relevant pattern, including extension methods. You can probably imagine what an extension method for deconstruction looks like, but the following listing gives a concrete example, using DateTime.

Listing 12.8. Using an extension method to deconstruct DateTime
static void Deconstruct(                           1
    this DateTime dateTime,                        1
    out int year, out int month, out int day) =>   1
    (year, month, day) =                           1
    (dateTime.Year, dateTime.Month, dateTime.Day); 1

static void Main()
{
    DateTime now = DateTime.UtcNow;
    var (year, month, day) = now;                  2
    Console.WriteLine(
        $"{year:0000}-{month:00}-{day:00}");       3
}

  • 1 Extension method to deconstruct DateTime
  • 2 Deconstructs the current date to year/month/day
  • 3 Displays the date using the three variables

As it happens, this is a private extension method declared in the same (static) class that you’re using it from, but it’d more commonly be public or internal, just like most extension methods are.

What if you want to deconstruct a DateTime to more than just a date? This is where overloading is useful. You can have two methods with different parameter lists, and the compiler will work out which to use based on the number of parameters. Let’s add another extension method to deconstruct a DateTime in terms of time as well as date and then use both our methods to deconstruct different values.

Listing 12.9. Using Deconstruct overloads
static void Deconstruct(                                1
    this DateTime dateTime,                             1
    out int year, out int month, out int day) =>        1
    (year, month, day) =                                1
    (dateTime.Year, dateTime.Month, dateTime.Day);      1

static void Deconstruct(                                2
    this DateTime dateTime,                             2
    out int year, out int month, out int day,           2
    out int hour, out int minute, out int second) =>    2
    (year, month, day, hour, minute, second) =          2
    (dateTime.Year, dateTime.Month, dateTime.Day,       2
    dateTime.Hour, dateTime.Minute, dateTime.Second);   2

static void Main()
{
    DateTime birthday = new DateTime(1976, 6, 19);
    DateTime now = DateTime.UtcNow;

    var (year, month, day, hour, minute, second) = now; 3
    (year, month, day) = birthday;                      4
}

  • 1 Deconstructs a date to year/month/day
  • 2 Deconstructs a date to year/month/day/hour/minute/second
  • 3 Uses the six-value deconstructor
  • 4 Uses the three-value deconstructor

You can use extension Deconstruct methods for types that already have instance Deconstruct methods, and they’ll be used if the instance methods aren’t applicable when deconstructing, just as for normal method calls.

The restrictions for an extension Deconstruct method follow naturally from those of an instance method:

  • It has to be accessible to the calling code.
  • Other than the first parameter (the target of the extension method), all parameters must be out parameters.
  • There must be at least two such out parameters.
  • The method may be generic, but only the receiver of the call (the first parameter) can participate in type inference.

The rules indicating when a method can and can’t be generic deserve closer scrutiny, particularly because they also shed light on why you need to use a different number of parameters when overloading Deconstruct. The key lies in how the compiler treats the Deconstruct method.

12.2.3. Compiler handling of Deconstruct calls

When everything’s working as expected, you can get away without thinking too much about how the compiler decides which Deconstruct method to use. If you run into problems, however, it can be useful to try to put yourself in the place of the compiler.

The timing you’ve already seen for tuple decomposition still applies when deconstructing with methods, so I’ll focus on the method call itself. Let’s take a somewhat concrete example, working out what the compiler does when faced with a deconstruction like this:

(int x, string y) = target;

I say this is a somewhat concrete example because I haven’t shown what the type of target is. That’s deliberate, because all you need to know is that it isn’t a tuple type. The compiler expands this into something like this:

target.Deconstruct(out var tmpX, out var tmpY);
int x = tmpX;
string y = tmpY;

It then uses all the normal rules of method invocation to try to find the right method to call. I realize that the use of out var is something you haven’t seen before. You’ll look at it more closely in section 14.2, but all you need to know for now is that it’s declaring an implicitly typed variable using the type of the out parameter to infer the type.

The important thing to notice is that the types of the variables you’ve declared in the original code aren’t used as part of the Deconstruct call. That means they can’t participate in type inference. This explains three things:

  • Instance Deconstruct methods can’t be generic, because there’s no information for type inference to use.
  • Extension Deconstruct methods can be generic, because the compiler may be able to infer type arguments using target, but that’s the only parameter that’s going to be useful in terms of type inference.
  • When overloading Deconstruct methods, it’s the number of out parameters that’s important, not their type. If you introduce multiple Deconstruct methods with the same number of out parameters, that’s just going to stop the compiler from using any of them, because the calling code won’t be able to tell which one you mean.

I’ll leave it at that, because I don’t want to make more of this than needed. If you run into problems that you can’t understand, try performing the transformation shown previously, and it may well make things clearer.

That’s everything you need to know about deconstruction. The rest of the chapter focuses on pattern matching, a feature that’s theoretically entirely separate from deconstruction but has a similar feeling to it in terms of the tools available for using existing data in new ways.

12.3. Introduction to pattern matching

Like many other features, pattern matching is new to C# but not new to programming languages in general. In particular, functional languages often make heavy use of patterns. The patterns in C# 7.0 satisfy many of the same use cases but in a manner that fits in with the rest of the syntax of the language.

The basic idea of a pattern is to test a certain aspect of a value and use the result of that test to perform another action. Yes, that sounds just like an if statement, but patterns are typically used either to give more context for the condition or to provide more context within the action itself based on the pattern. Yet again, this feature doesn’t allow you to do anything you couldn’t do before; it just lets you express the same intention more clearly.

I don’t want to go too far without giving an example. Don’t worry if it seems a little odd right now; the aim is to give you a flavor. Suppose you have an abstract class Shape that defines an abstract Area property and derived classes Rectangle, Circle, and Triangle. Unfortunately, for your current application, you don’t need the area of a shape; you need its perimeter. You may not be able to modify Shape to add a Perimeter property (you may not have any control over its source at all), but you know how to compute it for all the classes you’re interested in. Before C# 7, a Perimeter method might look something like the following listing.

Listing 12.10. Computing a perimeter without patterns
static double Perimeter(Shape shape)
{
    if (shape == null)
        throw new ArgumentNullException(nameof(shape));
    Rectangle rect = shape as Rectangle;
    if (rect != null)
        return 2 * (rect.Height + rect.Width);
    Circle circle = shape as Circle;
    if (circle != null)
        return 2 * PI * circle.Radius;
    Triangle triangle = shape as Triangle;
    if (triangle != null)
        return triangle.SideA + triangle.SideB + triangle.SideC;
    throw new ArgumentException(
        $"Shape type {shape.GetType()} perimeter unknown", nameof(shape));
}
Note

If the lack of curly braces inside offends you, I apologize. I normally use them for all loops, if statements, and so forth, but in this case, they ended up dwarfing the useful code here and in some other later pattern examples. I’ve removed them for brevity.

That’s ugly. It’s repetitive and long-winded; the same pattern of “check whether the shape is a particular type, and then use that type’s properties” occurs three times. Urgh. Importantly, even though there are multiple if statements here, the body of each of them returns a value, so you’re always picking only one of them to execute. The following listing shows how the same code can be written in C# 7 using patterns in a switch statement.

Listing 12.11. Computing a perimeter with patterns
static double Perimeter(Shape shape)
{
    switch (shape)
    {
        case null:                                            1
            throw new ArgumentNullException(nameof(shape));   1
        case Rectangle rect:                                  2
            return 2 * (rect.Height + rect.Width);            2
        case Circle circle:                                   2
            return 2 * PI * circle.Radius;                    2
        case Triangle tri:                                    2
            return tri.SideA + tri.SideB + tri.SideC;         2
        default:                                              3
            throw new ArgumentException(...);                 3
    }
}

  • 1 Handles a null value
  • 2 Handles each type you know about
  • 3 If you don’t know what to do, throw an exception.

This is quite a departure from the switch statement from previous versions of C#, in which case labels were all just constant values. Here you’re sometimes interested in just value matching (for the null case) and sometimes interested in the type of the value (the rectangle, circle, and triangle cases). When you match by type, that match also introduces a new variable of that type that you use to calculate the perimeter.

The topic of patterns within C# has two distinct aspects:

  • The syntax for patterns
  • The contexts in which you can use patterns

At first, it may feel like everything’s new, and differentiating between these two aspects may seem pointless. But the patterns you can use in C# 7.0 are just the start: the C# design team has been clear that the syntax has been designed for new patterns to become available over time. When you know the places in the language where patterns are allowed, you can pick up new patterns easily. It’s a little bit chicken and egg—it’s hard to demonstrate one part without showing the other—but we’ll start by looking at the kinds of patterns available in C# 7.0.

12.4. Patterns available in C# 7.0

C# 7.0 introduces three kinds of patterns: constant patterns, type patterns, and the var pattern. I’m going to demonstrate each with the is operator, which is one of the contexts for using patterns.

Every pattern tries to match an input. This can be any nonpointer expression. For the sake of simplicity, I’ll refer to this as input in the pattern descriptions, as if it were a variable, but it doesn’t have to be.

12.4.1. Constant patterns

A constant pattern is just what it sounds like: the pattern consists entirely of a compile-time constant expression, which is then checked for equality with input. If both input and the constant are integer expressions, they’re compared using ==. Otherwise, the static object.Equals method is called. It’s important that it’s the static method that’s called, because that enables you to safely check for a null value. The following listing shows an example that serves even less real-world purpose than most of the other examples in the book, but it does demonstrate a couple of interesting points.

Listing 12.12. Simple constant matches
static void Match(object input)
{
    if (input is "hello")
        Console.WriteLine("Input is string hello");
    else if (input is 5L)
        Console.WriteLine("Input is long 5");
    else if (input is 10)
        Console.WriteLine("Input is int 10");
    else
        Console.WriteLine("Input didn't match hello, long 5 or int 10");
}
static void Main()
{
    Match("hello");
    Match(5L);
    Match(7);
    Match(10);
    Match(10L);
}

The output is mostly straightforward, but you may be surprised by the penultimate line:

Input is string hello
Input is long 5
Input didn't match hello, long 5 or int 10
Input is int 10
Input didn't match hello, long 5 or int 10

If integers are compared using ==, why didn’t the last call of Match(10L) match? The answer is that the compile-time type of input isn’t an integral type, it’s just object, so the compiler generates code equivalent to calling object.Equals(x, 10). That returns false when the value of x is a boxed Int64 instead of a boxed Int32, as is the case in our last call to Match. For an example using ==, you’d need something like this:

long x = 10L;
if (x is 10)
{
    Console.WriteLine("x is 10");
}

This isn’t useful in is expressions like this; it’d be more likely to be used in switch, where you might have some integer constants (like a pre-pattern-matching switch statement) along with other patterns. A more obviously useful kind of pattern is the type pattern.

12.4.2. Type patterns

A type pattern consists of a type and an identifier—a bit like a variable declaration. The pattern matches if input is a value of that type, just like the regular is operator. The benefit of using a pattern for this is that it also introduces a new pattern variable of that type initialized with the value if the pattern matches. If the pattern doesn’t match, the variable still exists; it’s just not definitely assigned. If input is null, it won’t match any type. As described in section 12.1.1, the underscore identifier _ can be used, in which case it’s a discard and no variable is introduced. The following listing is a conversion of our earlier set of as-followed-by-if statements (listing 12.10) to use pattern matching without taking the more extreme step of using a switch statement.

Listing 12.13. Using type patterns instead of as/if
static double Perimeter(Shape shape)
{
    if (shape == null)
        throw new ArgumentNullException(nameof(shape));
    if (shape is Rectangle rect)
        return 2 * (rect.Height + rect.Width);
    if (shape is Circle circle)
        return 2 * PI * circle.Radius;
    if (shape is Triangle triangle)
        return triangle.SideA + triangle.SideB + triangle.SideC;
    throw new ArgumentException(
        $"Shape type {shape.GetType()} perimeter unknown", nameof(shape));
}

In this case, I definitely prefer the switch statement option instead, but that would be overkill if you had only one as/if to replace. A type pattern is generally used to replace either an as/if combination or if with is followed by a cast. The latter is required when the type you’re testing is a non-nullable value type.

The type specified in a type pattern can’t be a nullable value type, but it can be a type parameter, and that type parameter may end up being a nullable value type at execution time. In that case, the pattern will match only when the value is non-null. The following listing shows this using int? as a type argument for a method that uses the type parameter in a type pattern, even though the expression value is int? t wouldn’t have compiled.

Listing 12.14. Behavior of nullable value types in type patterns
static void Main()
{
    CheckType<int?>(null);
    CheckType<int?>(5);     
    CheckType<int?>("text");
    CheckType<string>(null);
    CheckType<string>(5);
    CheckType<string>("text");
}

static void CheckType<T>(object value)
{
    if (value is T t)
    {
        Console.WriteLine($"Yes! {t} is a {typeof(T)}");
    }
    else
    {
        Console.WriteLine($"No! {value ?? "null"} is not a {typeof(T)}");
    }
}

The output is as follows:

No! null is not a System.Nullable`1[System.Int32]
Yes! 5 is a System.Nullable`1[System.Int32]
No! text is not a System.Nullable`1[System.Int32]
No! null is not a System.String
No! 5 is not a System.String
Yes! text is a System.String

To wrap up this section on type patterns, there’s one issue in C# 7.0 that’s addressed by C# 7.1. It’s one of those cases where if your project is already set to use C# 7.1 or higher, you may not even notice. I’ve included this mostly so that you don’t get confused if you copy code from a C# 7.1 project to a C# 7.0 project and find it breaks.

In C# 7.0, type patterns like this

x is SomeType y

required that the compile-time type of x could be cast to SomeType. That sounds entirely reasonable until you start using generics. Consider the following generic method that displays details of the shapes provided using pattern matching.

Listing 12.15. Generic method using type patterns
static void DisplayShapes<T>(List<T> shapes) where T : Shape
{
    foreach (T shape in shapes)          1
    {
        switch (shape)                   2
        {
            case Circle c:               3
                Console.WriteLine($"Circle radius {c.Radius}");
                break;
            case Rectangle r:
                Console.WriteLine($"Rectangle {r.Width} x {r.Height}");
                break;
            case Triangle t:
                Console.WriteLine(
                    $"Triangle sides {t.SideA}, {t.SideB}, {t.SideC}");
                break;
        }
    }
}

  • 1 Variable type is a type parameter (T)
  • 2 Switches on that variable
  • 3 Tries to use type case to convert to concrete shape type

In C# 7.0, this listing won’t compile, because this wouldn’t compile either:

if (shape is Circle)
{
    Circle c = (Circle) shape;
}

The use of the is operator is valid, but the cast isn’t. The inability to cast type parameters directly has been an annoyance for a long time in C#, with the usual workaround being to first cast to object:

if (shape is Circle)
{
    Circle c = (Circle) (object) shape;
}

This is clumsy enough in a normal cast, but it’s worse when you’re trying to use an elegant type pattern.

In listing 12.15, this can be worked around by either accepting an IEnumerable<Shape> (taking advantage of generic covariance to allow a conversion of List<Circle> to IEnumerable<Shape>, for example) or by specifying the type of shape as Shape instead of T. In other cases, the workarounds aren’t as simple. C# 7.1 addresses this by permitting a type pattern for any type that would be valid using the as operator, which makes listing 12.15 valid.

I expect the type pattern to be the most commonly used pattern out of the three patterns introduced in C# 7.0. Our final pattern almost doesn’t sound like a pattern at all.

12.4.3. The var pattern

The var pattern looks like a type pattern but using var as the type, so it’s just var followed by an identifier:

someExpression is var x

Like type patterns, it introduces a new variable. But unlike type patterns, it doesn’t test anything. It always matches, resulting in a new variable with the same compile-time type as input, with the same value as input. Unlike type patterns, the var pattern still matches even if input is a null reference.

Because it always matches, using the var pattern with the is operator in an if statement in the way that I’ve demonstrated for the other patterns is reasonably pointless. It’s most useful with switch statements in conjunction with a guard clause (described in section 12.6.1), although it could also occasionally be useful if you want to switch on a more complex expression without assigning it to a variable.

Just for the sake of presenting an example of var without using guard clauses, listing 12.16 shows a Perimeter method similar to the one in listing 12.11. But this time, if the shape parameter has a null value, a random shape is created instead. You use a var pattern to report the type of the shape if you then can’t compute the perimeter. You don’t need the constant pattern with the value null now, as you’re ensuring that you never switch on a null reference.

Listing 12.16. Using the var pattern to introduce a variable on error
static double Perimeter(Shape shape)
{
    switch (shape ?? CreateRandomShape())
    {
        case Rectangle rect:
            return 2 * (rect.Height + rect.Width);
        case Circle circle:
            return 2 * PI * circle.Radius;
        case Triangle triangle:
            return triangle.SideA + triangle.SideB + triangle.SideC;
        case var actualShape:
            throw new InvalidOperationException(
                $"Shape type {actualShape.GetType()} perimeter unknown");
    }
}

In this case, an alternative would’ve been to introduce the actualShape variable before the switch statement, switch on that, and then use the default case as before.

Those are all the patterns available in C# 7.0. You’ve already seen both of the contexts in which they can be used—with the is operator and in switch statements—but there’s a little more to say in each case.

12.5. Using patterns with the is operator

The is operator can be used anywhere as part of a normal expression. It’s almost always used with if statements, but it certainly doesn’t have to be. Until C# 7, the right-hand side of an is operator had to be just a type, but now it can be any pattern. Although this does allow you to use the constant or var patterns, realistically you’ll almost always use type patterns instead.

Both the var pattern and type patterns introduce a new variable. Prior to C# 7.3, this came with an extra restriction: you can’t use them in field, property, or constructor initializers or query expressions. For example, this would be invalid:

static int length = GetObject() is string text ? text.Length : -1;

I haven’t found this to be an issue, but the restriction is lifted in C# 7.3 anyway.

That leaves us with patterns introducing local variables, which leads to an obvious question: what’s the scope of the newly introduced variable? I understand that this was the cause of a lot of discussion within the C# language team and the community, but the final result is that the scope of the introduced variable is the enclosing block.

As you might expect from a hotly debated topic, there are pros and cons to this. One of the things I’ve never liked about the as/if pattern shown in listing 12.10 is that you end up with a lot of variables in scope even though you typically don’t want to use them outside the condition where the value matched the type you were testing. Unfortunately, this is still the case when using type patterns. It’s not quite the same situation, as the variable won’t be definitely assigned in branches where the pattern wasn’t matched.

To compare, after this code

string text = input as string;
if (text != null)
{
    Console.WriteLine(text);
}

the text variable is in scope and definitely assigned. The roughly equivalent type pattern code looks like this:

if (input is string text)
{
    Console.WriteLine(text);
}

After this, the text variable is in scope, but not definitely assigned. Although this does pollute the declaration space, it can be useful if you’re trying to provide an alternative way of obtaining a value. For example:

if (input is string text)
{
    Console.WriteLine("Input was already a string; using that");
}
else if (input is StringBuilder builder)
{
    Console.WriteLine("Input was a StringBuilder; using that");    
    text = builder.ToString();
}
else
{
    Console.WriteLine(
        $"Unable to use value of type ${input.GetType()}. Enter text:");    
    text = Console.ReadLine();
}
Console.WriteLine($"Final result: {text}");

Here you really want the text variable to stay in scope, because you want to use it; you assign to it in one of two ways. You don’t really want builder in scope after the middle block, but you can’t have it both ways.

To be a little more technical about the definite assignment, after an is expression with a pattern that introduces a pattern variable, the variable is (in language specification terminology) “definitely assigned after true expression.” That can be important if you want an if condition to do more than just test the type. For example, suppose you want to check whether the value provided is a large integer. This is fine:

if (input is int x && x > 100)
{
    Console.WriteLine($"Input was a large integer: {x}");
}

You can use x after the && because you’ll evaluate that operand only if the first operand evaluates to true. You can also use x inside the if statement because you’ll execute the body of the if statement only if both && operands evaluate to true. But what if you want to handle both int or long values? You can test the value, but then you can’t tell which condition matched:

if ((input is int x && x > 100) || (input is long y && y > 100))
{
    Console.WriteLine($"Input was a large integer of some kind");
}

Here, both x and y are in scope both inside and after the if statement, even though the part declaring y looks as if it may not execute. But the variables are definitely assigned only within the very small piece of code where you’re checking how large the values are.

All of this makes logical sense, but it can be a little surprising the first time you see it. The two takeaways of this section are as follows:

  • Expect the scope of a pattern variable declared in an is expression to be the enclosing block.
  • If the compiler prevents you from using a pattern variable, that means the language rules can’t prove that the variable will have been assigned a value at that point.

In the final part of this chapter, we’ll look at patterns used in switch statements.

12.6. Using patterns with switch statements

Specifications are often written not in terms of algorithms as such but in terms of cases. The following are examples far removed from computing:

  • Taxes and benefits—Your tax bracket probably depends on your income and some other factors.
  • Travel tickets—There may be group discounts as well as separate prices for children, adults, and the elderly.
  • Takeout food ordering—There can be deals if your order meets certain criteria.

In the past, we’ve had two ways of detecting which case applies to a particular input: switch statements and if statements, where switch statements were limited to simple constants. We still have just those two approaches, but if statements are already cleaner using patterns as you’ve seen, and switch statements are much more powerful.

Note

Pattern-based switch statements feel quite different from the constant-value-only switch statements of the past. Unless you’ve had experience with other languages that have similar functionality, you should expect it to take a little while to get used to the change.

switch statements with patterns are largely equivalent to a sequence of if/else statements, but they encourage you to think more in terms of “this kind of input leads to this kind of output” instead of steps.

All switch statements can be considered pattern based

Throughout this section, I talk about constant-based switch statements and pattern-based switch statements as if they’re different. Because constant patterns are patterns, every valid switch statement can be considered a pattern-based switch statement, and it will still behave in exactly the same way. The differences you’ll see later in terms of execution order and new variables being introduced don’t apply to constant patterns anyway.

I find it quite helpful, at least at the moment, to consider these as if they were two separate constructs that happen to use the same syntax. You may feel more comfortable not to make that distinction. It’s safe to use either mental model; they’ll both predict the code’s behavior correctly.

You’ve already seen an example of patterns in switch statements in section 12.3, where you used a constant pattern to match null and type patterns to match different kinds of shapes. In addition to simply putting a pattern in the case label, there’s one new piece of syntax to introduce.

12.6.1. Guard clauses

Each case label can also have a guard clause, which consists of an expression:

case pattern when expression:

The expression has to evaluate to a Boolean value[2] just like an if statement’s condition. The body of the case will be executed only if the expression evaluates to true. The expression can use more patterns, thereby introducing extra pattern variables.

2

It can also be a value that can be implicitly converted to a Boolean value or a value of a type that provides a true operator. These are the same requirements as the condition in an if statement.

Let’s look at a concrete example that’ll also illustrate my point about specifications. Consider the following definition of the Fibonacci sequence:

  • fib(0) = 0
  • fib(1) = 1
  • fib(n) = fib(n-2) + fib(n-1) for all n > 1

In chapter 11, you saw how to generate the Fibonacci sequence by using tuples, which is a clean approach when considering it as a sequence. If you consider it only as a function, however, the preceding definition leads to the following listing: a simple switch statement using patterns and a guard clause.

Listing 12.17. Implementing the Fibonacci sequence recursively with patterns
static int Fib(int n)
{
    switch (n)
    {
        case 0: return 0;                                       1
        case 1: return 1;                                       1
        case var _ when n > 1: return Fib(n - 2) + Fib(n - 1);  2
        default: throw new ArgumentOutOfRangeException(         3
            nameof(n), "Input must be non-negative");           3
    }                                                           3
}

  • 1 Base cases handled with constant patterns
  • 2 Recursive case handled with var pattern and guard clause
  • 3 If you don’t match any patterns, the input was invalid.

This is a horribly inefficient implementation that I’d never use in real life, but it clearly demonstrates how a specification can be directly translated into code.

In this example, the guard clause doesn’t need to use the pattern variable, so I used a discard with the _ identifier. In many cases, if the pattern introduces a new variable, it will be used in the guard clause or at least in the case body.

When you use guard clauses, it makes perfect sense for the same pattern to appear multiple times, because the first time the pattern matches, the guard clause may evaluate to false. Here’s an example from Noda Time in a tool used to build documentation:

private string GetUid(TypeReference type, bool useTypeArgumentNames)
{
    switch (type)
    {
        case ByReferenceType brt:
            return $"{GetUid(brt.ElementType, useTypeArgumentNames)}@";
        case GenericParameter gp when useTypeArgumentNames:
            return gp.Name;
        case GenericParameter gp when gp.DeclaringType != null:
            return $"`{gp.Position}";
        case GenericParameter gp when gp.DeclaringMethod != null:
            return $"``{gp.Position}";
        case GenericParameter gp:
            throw new InvalidOperationException(
                "Unhandled generic parameter");
        case GenericInstanceType git:
            return "(This part of the real code is long and irrelevant)";
        default:
            return type.FullName.Replace('/', '.');
    }
}

I have four patterns that handle generic parameters based on the useTypeArgumentNames method parameter and then whether the generic type parameter was introduced in a method or a type. The case that throws an exception is almost a default case for generic parameters, indicating that it’s come across a situation I haven’t thought about yet. The fact that I’m using the same pattern variable name (gp) for multiple cases raises another natural question: what’s the scope of a pattern variable introduced in a case label?

12.6.2. Pattern variable scope for case labels

If you declare a local variable directly within a case body, the scope of that variable is the whole switch statement, including other case bodies. That’s still true (and unfortunate, in my opinion), but it doesn’t include variables declared within case labels. The scope of those variables is just the body associated with that case label. That applies to pattern variables declared by the pattern, pattern variables declared within the guard clause, and any out variables (see section 14.2) declared in the guard clause.

That’s almost certainly what you want, and it’s useful in terms of allowing you to use the same pattern variables for multiple cases handling similar situations, as demonstrated in the Noda Time tool code. There’s one quirk here: just as with normal switch statements, it’s valid to have multiple case labels with the same body. At that point, the variables declared within all the case labels for that body are required to have different names (because they’re contributing to the same declaration space). But within the case body, none of those variables will be definitely assigned, because the compiler can’t tell which label matched. It can still be useful to introduce those variables, but mostly for the sake of using them in guard clauses.

For example, suppose you’re matching an object input, and you want to make sure that if it’s numeric, it’s within a particular range, and that range may vary by type. You could use one type pattern per numeric type with a corresponding guard clause. The following listing shows this for int and long, but you could expand it for other types.

Listing 12.18. Using multiple case labels with patterns for a single case body
static void CheckBounds(object input)
{
    switch (input)
    {
        case int x when x > 1000:
        case long y when y > 10000L:
            Console.WriteLine("Value is too large");
            break;
        case int x when x < -1000:
        case long y when y < -10000L:
            Console.WriteLine("Value is too low");
            break;
        default:
            Console.WriteLine("Value is in range");
            break;
    }
}

The pattern variables are definitely assigned within the guard clauses, because execution will reach the guard clause only if the pattern has matched to start with, and they’re still in scope within the body, but they’re not definitely assigned. You could assign new values to them and use them after that, but I feel that won’t often be useful.

In addition to the basic premise of pattern matching being new and different, there’s one huge difference between the constant-based switch statements of the past and new pattern-based switch statements: the order of cases matters in a way that it didn’t before.

12.6.3. Evaluation order of pattern-based switch statements

In almost all situations, case labels for constant-based switch statements can be reordered freely with no change in behavior.[3] This is because each case label matches a single constant value, and the constants used for any switch statement all have to be different, so any input can match at most only one case label. With patterns, that’s no longer true.

3

The only time this isn’t true is when you use a variable in one case body that was declared in an earlier case body. That’s almost always a bad idea anyway, and it’s a problem only because of the shared scope of such variables.

The logical evaluation order of a pattern-based switch statement can be summarized simply:

  • Each case label is evaluated in source-code order.
  • The code body of the default label is executed only when all the case labels have been evaluated, regardless of where the default label is within the switch statement.
Tip

Although you now know that the code associated with the default label is executed only if none of the case labels matches, regardless of where it appears, it’s possible that some people reading your code might not. (Indeed, you might have forgotten it by the time you next come to read your own code.) If you put the default label as the final part of the switch statement, the behavior is always clear.

Sometimes it won’t matter. In our Fibonacci-computing method, for example, the cases were only 0, 1, and more than 1, so they could be freely reordered. Our Noda Time tool code, however, had four cases that definitely need to be checked in order:

case GenericParameter gp when useTypeArgumentNames:
    return gp.Name;
case GenericParameter gp when gp.DeclaringType != null:
    return $"`{gp.Position}";
case GenericParameter gp when gp.DeclaringMethod != null:
    return $"``{gp.Position}";
case GenericParameter gp:
    throw new InvalidOperationException(...);

Here you want to use the generic type parameter name whenever useTypeArgumentNames is true (the first case), regardless of the other cases. The second and third cases are mutually exclusive (in a way that you know but the compiler wouldn’t), so their order doesn’t matter. The last case must be last within these four because you want the exception to be thrown only if the input is a GenericParameter that isn’t otherwise handled.

The compiler is helpful here: the final case doesn’t have a guard clause, so it’ll always be valid if the type pattern matches. The compiler is aware of this; if you put that case earlier than the other case labels with the same pattern, it knows that’s effectively hiding them and reports an error.

Multiple case bodies can be executed in only one way, and that’s with the rarely used goto statement. That’s still valid within pattern-based switch statements, but you can goto only a constant value, and a case label must be associated with that value without a guard clause. For example, you can’t goto a type pattern, and you can’t goto a value on the condition that an associated guard clause also evaluates to true. In reality, I’ve seen so few goto statements in switch statements that I can’t see this being much of a restriction.

I deliberately referred to the logical evaluation order earlier. Although the C# compiler could effectively translate every switch statement into a sequence of if/else statements, it can act more efficiently than that. For example, if there are multiple type patterns for the same type but with different guard clauses, it can evaluate the type pattern part once and then check each guard clause in turn instead. Similarly, for constant values without guard patterns (which still have to be distinct, just as in previous versions of C#), the compiler can use the IL switch instruction, potentially after performing an implicit type check. Exactly which optimizations the compiler performs is beyond the scope of this book, but if you ever happen to look at the IL associated with a switch statement and it bears little resemblance to the source code, this may well be the cause.

12.7. Thoughts on usage

This section provides preliminary thoughts on how the features described in this chapter are best used. Both features are likely to evolve further and possibly even be combined with a deconstruction pattern. Other related potential features, such as syntax to write an expression-bodied method for which the result is based on a pattern-based switch, may well affect where these features are used. You’ll see some potential C# 8 features like this in chapter 15.

Pattern matching is an implementation concern, which means that you don’t need to worry if you find later that you’ve overused it. You can revert to an older style of coding if you find patterns don’t give you the readability benefit you’d expected. The same is true of deconstruction to some extent. But if you’ve added public Deconstruct methods all over your API, removing them would be a breaking change.

More than that, I suggest that most types aren’t naturally deconstructable anyway, just as most types don’t have a natural IComparable<T> implementation. I suggest adding a Deconstruct method only if the order of the components is obvious and unambiguous. That’s fine for coordinates, anything with a hierarchical nature such as date/time values, or even where there’s a common convention, such as colors being thought of as RGB with optional alpha. Most business-related entities probably don’t fall into this category, though; for example, an item in an online shopping basket has various aspects, but there’s no obvious order to them.

12.7.1. Spotting deconstruction opportunities

The simplest kind of deconstruction to use is likely to be related to tuples. If you’re calling a method that returns a tuple and you don’t need to keep the values together, consider deconstructing them instead. For example, with our MinMax method from chapter 11, I’d almost always deconstruct immediately instead of keeping the return value as a tuple:

int[] values = { 2, 7, 3, -5, 1, 0, 10 };
var (min, max) = MinMax(values);
Console.WriteLine(min);
Console.WriteLine(max);

I suspect the use of nontuple deconstruction will be rarer, but if you’re dealing with points, colors, date/time values, or something similar, you may find that it’s worth deconstructing the value early on if you’d otherwise refer to the components via properties multiple times. You could’ve done this before C# 7, but the ease of declaring multiple local variables via deconstruction could easily swing the balance between not worth doing and worth doing.

12.7.2. Spotting pattern matching opportunities

You should consider pattern matching in two obvious places:

  • Anywhere you’re using the is or as operators and conditionally executing code by using the more specifically typed value.
  • Anywhere you have an if/else-if/else-if/else sequence using the same value for all the conditions, and you can use a switch statement instead.

If you find yourself using a pattern of the form var ... when multiple times (in other words, when the only condition occurs in a guard clause), you may want to ask yourself whether this is really pattern matching. I’ve certainly come across scenarios like that, and so far I’ve erred on the side of using pattern matching anyway. Even if it feels slightly abusive, it conveys the intent of matching a single condition and taking a single action more clearly than the if/else sequence does, in my view.

Both of these are transformations of an existing code structure with changes only to the implementation details. They’re not changing the way you think about and organize your logic. That grander style of change—which could still be refactoring within the visible API of a single type, or perhaps within the public API of an assembly, by changing internal details—is harder to spot. Sometimes it may be a move away from using inheritance; the logic for a calculation may be more clearly expressed in a single place that considers all the different cases than as part of the type representing each of those cases. The perimeter of a shape case in section 12.3 is one example of this, but you could easily apply the same ideas to many business cases. This is where disjoint union types are likely to become more widespread within C#.

As I said, these are preliminary thoughts. As always, I encourage you to experiment with deliberate introspection: consider opportunities as you code, and if you try something new, reflect on its pros and cons after you’ve done so.

Summary

  • Deconstruction allows you to break values into multiple variables with syntax that’s consistent between tuples and nontuples.
  • Nontuple types are deconstructed using a Deconstruct method with out parameters. This can be an extension method or an instance method.
  • Multiple variables can be declared with a single var deconstruction if all the types can be inferred by the compiler.
  • Pattern matching allows you to test the type and content of a value, and some patterns allow you to declare a new variable.
  • Pattern matching can be used with the is operator or in switch statements.
  • A pattern within a switch statement can have an additional guard clause introduced by the when contextual keyword.
  • When a switch statement contains patterns, the order of the case labels can change the behavior.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset