Pull to refresh
0
Microsoft
Microsoft — мировой лидер в области ПО и ИТ-услуг

Do more with patterns in C# 8.0

Reading time 6 min
Views 9.4K

Visual Studio 2019 Preview 2 is out! And with it, a couple more C# 8.0 features are ready for you to try. It’s mostly about pattern matching, though I’ll touch on a few other news and changes at the end.


Original in Blog

More patterns in more places


When C# 7.0 introduced pattern matching we said that we expected to add more patterns in more places iin the future. That time has come! We’re adding what we call recursive patterns, as well as a more compact expression form of switch statements called (you guessed it!) switch expressions.


Here’s a simple C# 7.0 example of patterns to start us out:


class Point
{
    public int X { get; }
    public int Y { get; }
    public Point(int x, int y) => (X, Y) = (x, y);
    public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}

static string Display(object o)
{
    switch (o)
    {
        case Point p when p.X == 0 && p.Y == 0:
            return "origin";
        case Point p:
            return $"({p.X}, {p.Y})";
        default:
            return "unknown";
    }
}

Switch expressions


First, let’s observe that many switch statements really don’t do much interesting work within the case bodies. Often they all just produce a value, either by assigning it to a variable or by returning it (as above). In all those situations, the switch statement is frankly rather clunky. It feels like the 5-decades-old language feature it is, with lots of ceremony.


We decided it was time to add an expression form of switch. Here it is, applied to the above example:


static string Display(object o)
{
    return o switch
    {
        Point p when p.X == 0 && p.Y == 0 => "origin",
        Point p                           => $"({p.X}, {p.Y})",
        _                                 => "unknown"
    };
}

There are several things here that changed from switch statements. Let’s list them out:


  • The switch keyword is «infix» between the tested value and the {...} list of cases. That makes it more compositional with other expressions, and also easier to tell apart visually from a switch statement.
  • The case keyword and the : have been replaced with a lambda arrow => for brevity.
  • default has been replaced with the _ discard pattern for brevity.
  • The bodies are expressions! The result of the selected body becomes the result of the switch expression.

Since an expression needs to either have a value or throw an exception, a switch expression that reaches the end without a match will throw an exception. The compiler does a great job of warning you when this may be the case, but will not force you to end all switch expressions with a catch-all: you may know better!


Of course, since our Display method now consists of a single return statement, we can simplify it to be expression-bodied:


 static string Display(object o) => o switch
    {
        Point p when p.X == 0 && p.Y == 0 => "origin",
        Point p                           => $"({p.X}, {p.Y})",
        _                                 => "unknown"
    };

To be honest, I am not sure what formatting guidance we will give here, but it should be clear that this is a lot terser and clearer, especially because the brevity typically allows you to format the switch in a «tabular» fashion, as above, with patterns and bodies on the same line, and the =>s lined up under each other.


By the way, we plan to allow a trailing comma , after the last case in keeping with all the other «comma-separated lists in curly braces» in C#, but Preview 2 doesn’t yet allow that.


Property patterns


Speaking of brevity, the patterns are all of a sudden becoming the heaviest elements of the switch expression above! Let’s do something about that.


Note that the switch expression uses the type pattern Point p (twice), as well as a when clause to add additional conditions for the first case.


In C# 8.0 we’re adding more optional elements to the type pattern, which allows the pattern itself to dig further into the value that’s being pattern matched. You can make it a property pattern by adding {...}‘s containing nested patterns to apply to the value’s accessible properties or fields. This let’s us rewrite the switch expression as follows:


static string Display(object o) => o switch
{
    Point { X: 0, Y: 0 }         p => "origin",
    Point { X: var x, Y: var y } p => $"({x}, {y})",
    _                              => "unknown"
};

Both cases still check that o is a Point. The first case then applies the constant pattern 0 recursively to the X and Y properties of p, checking whether they have that value. Thus we can eliminate the when clause in this and many common cases.


The second case applies the var pattern to each of X and Y. Recall that the var pattern in C# 7.0 always succeeds, and simply declares a fresh variable to hold the value. Thus x and y get to contain the int values of p.X and p.Y.


We never use p, and can in fact omit it here:


   Point { X: 0, Y: 0 }         => "origin",
    Point { X: var x, Y: var y } => $"({x}, {y})",
    _                            => "unknown"

One thing that remains true of all type patterns including property patterns, is that they require the value to be non-null. That opens the possibility of the «empty» property pattern {} being used as a compact «not-null» pattern. E.g. we could replace the fallback case with the following two cases:


   {}                           => o.ToString(),
    null                         => "null"

The {} deals with remaining nonnull objects, and null gets the nulls, so the switch is exhaustive and the compiler won’t complain about values falling through.


Positional patterns


The property pattern didn’t exactly make the second Point case shorter, and doesn’t seem worth the trouble there, but there’s more that can be done.


Note that the Point class has a Deconstruct method, a so-called deconstructor. In C# 7.0, deconstructors allowed a value to be deconstructed on assignment, so that you could write e.g.:


(int x, int y) = GetPoint(); // split up the Point according to its deconstructor

C# 7.0 did not integrate deconstruction with patterns. That changes with positional patterns which are an additional way that we are extending type patterns in C# 8.0. If the matched type is a tuple type or has a deconstructor, we can use positional patterns as a compact way of applying recursive patterns without having to name properties:


static string Display(object o) => o switch
{
    Point(0, 0)         => "origin",
    Point(var x, var y) => $"({x}, {y})",
    _                   => "unknown"
};

Once the object has been matched as a Point, the deconstructor is applied, and the nested patterns are applied to the resulting values.


Deconstructors aren’t always appropriate. They should only be added to types where it’s really clear which of the values is which. For a Point class, for instance, it’s safe and intuitive to assume that the first value is X and the second is Y, so the above switch expression is intuitive and easy to read.


Tuple patterns


A very useful special case of positional patterns is when they are applied to tuples. If a switch statement is applied to a tuple expression directly, we even allow the extra set of parentheses to be omitted, as in switch (x, y, z) instead of switch ((x, y, z)).


Tuple patterns are great for testing multiple pieces of input at the same time. Here is a simple implementation of a state machine:


static State ChangeState(State current, Transition transition, bool hasKey) =>
    (current, transition) switch
    {
        (Opened, Close)              => Closed,
        (Closed, Open)               => Opened,
        (Closed, Lock)   when hasKey => Locked,
        (Locked, Unlock) when hasKey => Closed,
        _ => throw new InvalidOperationException($"Invalid transition")
    };

Of course we could opt to include hasKey in the switched-on tuple instead of using when clauses – it is really a matter of taste:


static State ChangeState(State current, Transition transition, bool hasKey) =>
    (current, transition, hasKey) switch
    {
        (Opened, Close,  _)    => Closed,
        (Closed, Open,   _)    => Opened,
        (Closed, Lock,   true) => Locked,
        (Locked, Unlock, true) => Closed,
        _ => throw new InvalidOperationException($"Invalid transition")
    };

All in all I hope you can see that recursive patterns and switch expressions can lead to clearer and more declarative program logic.


Other C# 8.0 features in Preview 2


While the pattern features are the major ones to come online in VS 2019 Preview 2, There are a few smaller ones that I hope you will also find useful and fun. I won’t go into details here, but just give you a brief description of each.


Using declarations


In C#, using statements always cause a level of nesting, which can be highly annoying and hurt readability. For the simple cases where you just want a resource to be cleaned up at the end of a scope, you now have using declarations instead. Using declarations are simply local variable declarations with a using keyword in front, and their contents are disposed at the end of the current statement block. So instead of:


static void Main(string[] args)
{
    using (var options = Parse(args))
    {
        if (options["verbose"]) { WriteLine("Logging..."); }
        ...
    } // options disposed here
}

You can simply write


static void Main(string[] args)
{
    using var options = Parse(args);
    if (options["verbose"]) { WriteLine("Logging..."); }

} // options disposed here

Disposable ref structs


Ref structs were introduced in C# 7.2, and this is not the place to reiterate their usefulness, but in return they come with some severe limitations, such as not being able to implement interfaces. Ref structs can now be disposable without implementing the IDisposable interface, simply by having a Dispose method in them.


Static local functions


If you want to make sure your local function doesn’t incur the runtime costs associated with «capturing» (referencing) variables from the enclosing scope, you can declare it as static. Then the compiler will prevent reference of anything declared in enclosing functions – except other static local functions!


Changes since Preview 1


The main features of Preview 1 were nullable reference types and async streams. Both have evolved a bit in Preview 2, so if you’ve started using them, the following is good to be aware of.


Nullable reference types


We’ve added more options to control nullable warnings both in source (through #nullable and #pragma warning directives) and at the project level. We also changed the project file opt-in to <NullableContextOptions>enable</NullableContextOptions>.


Async streams


We changed the shape of the IAsyncEnumerable<T> interface the compiler expects! This brings the compiler out of sync with the interface provided in .NET Core 3.0 Preview 1, which can cause you some amount of trouble. However, .NET Core 3.0 Preview 2 is due out shortly, and that brings the interfaces back in sync.


Have at it!


As always, we are keen for your feedback! Please play around with the new pattern features in particular. Do you run into brick walls? Is something annoying? What are some cool and useful scenarios you find for them? Hit the feedback button and let us know!


Happy hacking,


Mads Torgersen, design lead for C#

Tags:
Hubs:
+16
Comments 5
Comments Comments 5

Articles

Information

Website
www.microsoft.com
Registered
Founded
Employees
Unknown
Location
США