42

C# 8: Switch expressions

 5 years ago
source link: https://www.tuicool.com/articles/hit/neYZRnz
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

At the end of January the .NET Core development team has released a new version of the .NET Core framework, .NET Core 3 preview 2. It delivers a few new C# features to developers. It is nice to see the language improving and I like all of them. But today I would like to talk about only one and it is "switch expressions". What is special about this feature is that it demonstrates the trend in language design. I do apologise for a bit subjective nature of explaining this trend, but it is how I see it and my view is based on the years of experience.

The new switch expression is quite simple. Anyone familiar with the switch statement can say what the following code is doing:

// Initialise result with calculated value that depends
// on operation, "+" for addition, "-" for subtraction,
// "/" for division.
var result = operation switch
{
    "+" => a + b,
    "-" => a - b,
    "/" => a / b
};

What immediately captures attention is absence of the case and break (or return) keywords. That's true, they are not necessary for switch expressions. What is less obvious is that what goes after the arrow (=>) is an expression. Like the right part of the assignment.

If you are familiar with the switch statement, you might ask how to define a "default" case. Since the C# 7 the switch cases can use pattern matching, so instead of using the "default" keyword, the match-all pattern can be used. It is just the "_" sign (underscore).

// Initialise result with calculated value that depends
// on operation, "+" for addition, "-" for subtraction,
// "/" for division. Throws NotSupportedException
// when the operation is not recognised.
var result = operation switch
{
    "+" => a + b,
    "-" => a - b,
    "/" => a / b,
    _ => throw new NotSupportedException()
};

The patterns are matched one-by-one from top to bottom so it is not a very good idea to put "match-all" above any other match. It will suppress them. Actually, C# compiler does a good job and warns about it. If you tried to do so, most likely you will get the following error: CS8510: The pattern has already been handled by a previous arm of the switch expression.

It should be said that all expressions of the switch expression cases should be calculated to the same type (unless they throw an exception). For example, all ints. Or strings. The switch statement is similar to the ternary operator in this case.

Another question that may rise as how to run multiple commands in the statement. Like logging or file operations. Or sometimes a long expression might benefit from splitting it into chunks, each assigned to own variable. Unfortunately, now there is no nice solution for that. The closest one to the ideal solution (although it is quite far from it) is using lambdas.

// Initialise result with calculated value that depends
// on operation, "+" for addition, "-" for subtraction,
// "/" for division. Throws NotSupportedException
// when the operation is not recognised. All supported
// operations are logged.
var result = operation switch
{
    "+" => ((Func<int>)(() => {
        Log("addition");
        return a + b;
    }))(),
    "-" => ((Func<int>)(() => {
        Log("subtraction");
        return a - b;
    }))(),
    "/" => ((Func<int>)(() => {
        Log("division");
        return a / b;
    }))(),
    _ => throw new NotSupportedException()
};

At the beginning I've mentioned that the most important observation I've made about this change is that it demonstrates the trend. So what this trend is about?

First of all, why the new expression if there are already if and switch statement and ternary operator? They can do the same and they already exist. The answer, I think, is a problem of initialisation and nullability. The typical pattern in C# is to define variable first and later calculate its value. If it is a reference variable (object), then in many cases it starts with unsafe null value. And the point is, that unfortunately, due to initialisation mistakes, the unsafe value can slip and cause an error later.

// This example just demonstrates the concept.
// The tools nowadays are smart enough to warn
// you about missing enum case in switch or
// uninitialised variable.

// initialise variables
var a = context.getFirstValue();
var b = context.getSecondValue();
var op = default(string);
switch (context.getOperation()) {
    case Op.Div: op = "/"; break;
    case Op.Sub: op = "-"; break;
    case Op.Add: op = "+"; break;
    // the Op enum has Mul but it is omitted by mistake
}
// this will crash with NullReferenceException
Console.WriteLine($"{a} {op.ToUpper()} {b}");

The problem of initialising variables with unsafe or incorrect values could be

addressed with lambdas:

var op = ((Func<string>)(() => {
    switch (context.getOperation()) {
        case Op.Div: return "/";
        case Op.Sub: return "-";
        case Op.Add: return "+";
        // now you kind of enforced to
        // handle all values, otherwise
        // it will not compile
        default: throw new NotSupportedException();
    }
}))();
Console.WriteLine($"{a} {op.ToUpper()} {b}");

The new construction can address this problem and allows initialisation with complex logic, that required bulky constructions before.

var op = context.getOperation() switch {
    Op.Div => "/", Op.Sub => "-", Op.Add => "+",
    _ => throw new NotSupportedException()
};
Console.WriteLine($"{a} {op.ToUpper()} {b}");

Thank you for reading. I've recently published course on debugging .NET memory leaks on Udemy (use code DOTNET to get a discount). It is on using LLDB for identifying memory leaks. Cool stuff, :star::star::star::star::star: . Check it out.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK