C# 7.0 – Patterns


Facebooktwittergoogle_pluslinkedinmail

The concept of patterns was introduced in C# 7.0. The idea is to check if an object reflects a specified shape.

Note: You can view all code examples on GitHub. My environment: Visual Studio 2017 and .NET Framework 4.6.1.

Constant patterns

There are a few types of patterns available. The simplest is the constant pattern. It just means that an object is equal to a constant value.

        private void Example1()
        {
            object obj = 24;

            if (obj is 24)
            {
                Console.WriteLine("Object is 24");
            }

            if (obj is "someValue")
            {
                Console.WriteLine("Object is someValue");
            }
        }

In the above example, the pattern is used in the is statement. The other syntax that can use patterns is case clause in the switch statement.

        private void Example2()
        {
            object obj = "someValue";
            switch (obj)
            {
                case 24:
                    Console.WriteLine("Object is 24");
                    break;
                case "someValue":
                    Console.WriteLine("Object is someValue");
                    break;
            }
        }

The support of constant patterns in switch statements means that it’s possible to switch not only on primitive values anymore.

Type patterns

More interesting pattern is the type pattern. It checks if an object is of the specified type. If that’s true then it’s assigned to a new variable.

        private void Example3()
        {
            object obj = Console.Out;

            if (obj is TextWriter writer1)
            {
                writer1.WriteLine("Object is TextWriter");
            }

            switch (obj)
            {
                case TextWriter writer2:
                    writer2.WriteLine("Object is TextWriter");
                    break;
            }
        }

The scope of a variable in the is statement is the enclosing block. For the variable in the case clause – the case block.

Conditional case clauses

switch  statements also gained an ability to define a more specific definition of an object to match. With when keyword it possible to add a condition for matched type.

        private void Example4()
        {
            object obj = 24;
            switch (obj)
            {
                case int i when i > 50:
                    Console.WriteLine("More than 50");
                    break;
                case int i when i < 30:
                    Console.WriteLine("Less than 30");
                    break;
                case int i:
                    Console.WriteLine("Other value: " + i);
                    break;
            }
        }

The when can also be used for other purposes – like quick, inline logging.

        private static void Log(string message) => Console.WriteLine(message);
        private static bool LogAndEnterClause(string message) { Log(message); return true; }
        private static bool LogAndExitClause(string message) { Log(message); return false; }

        private void Example5()
        {
            Example5_RunSwitchStatementFor(24);
            Example5_RunSwitchStatementFor("csharp.today");
        }

        private void Example5_RunSwitchStatementFor(object obj) 
        {
            switch (obj)
            {
                case int i when LogAndExitClause("Just log that there was int: " + i):
                    Console.WriteLine("This will never be printed");
                    break;
                case string s when LogAndEnterClause("Need to handle string"):
                    Console.WriteLine("Do something with string: " + s);
                    break;
            }
        }

Considering conditional case clauses means that more than one clause might match a given object. That introduces one more feature – order of case clauses now matters! In C# 6.0 and before, the evaluation of case clauses was unspecified and not deterministic. That means that a developer couldn’t count on clauses being evaluated in any specific order. With C# 7.0 the order of clauses evaluated will be the same as their order in the switch statement. There is one exception to that – default clause will be always evaluated as the last one, no matter where it is in the switch statement.

        private void Example6()
        {
            var obj = 24;

            switch (obj)
            {
                case int i when i > 10:
                    // This will catch all int values above 10
                    break;
                case int i when i > 20:
                    // This won't match, because of the above case
                    break;
            }

            switch (obj)
            {
                case int i when i > 20:
                    // Values above 20
                    break;
                case int i when i > 10:
                    // Values: 11-20
                    break;
            }

            switch (obj)
            {
                default:
                    Console.WriteLine("Default case");
                    break;
                case int i:
                    Console.WriteLine("Int case");
                    break;
            }
        }

Null values

Next nice feature that comes with patterns is how null values are matched. If we try to match a null value object it won’t catch any type pattern. This might seem to be unexpected. But the reasoning behind this behavior is that otherwise, it would lead to matching first type pattern and could cause a lot of null reference exceptions.

There is the null constant pattern that matches null values.

        private void Example7()
        {
            object obj = null;

            if (obj is null)
            {
                Console.WriteLine("obj is null");
            }

            switch (obj)
            {
                case null:
                    Console.WriteLine("null case");
                    break;
            }

        }

It’s also the recommended way to test if an object is null because it’s not using equals operator. Meaning is resistant to an evil code that could override equals operator and change behavior for null compare.

Var patterns

Var pattern is the last pattern. It’s a bit strange – it always matches and puts the value into a new variable. It might be difficult to find a use case for it, but there is one. The var pattern becomes handy if a new variable is needed in a boolean expression.

        private void Example8()
        {
            var cusomers = Customer.GetCustomers().ToArray();
            Console.WriteLine("All customers: " + cusomers.Length);

            var selectedCusomers = cusomers
                .Where(c => c.CalculateBonus() is var bonus && bonus > 50 && bonus < 70);
            Console.WriteLine("Premium customers: " + selectedCusomers.Count());
        }

 

Facebooktwittergoogle_pluslinkedinmail

Leave a comment

Your email address will not be published. Required fields are marked *