left-icon

C# Features Succinctly®
by Dirk Strauss

Previous
Chapter

of
A
A
A

CHAPTER 4

The Future of C# and C# 9

The Future of C# and C# 9


There are some nice features planned for C# 9. While all these planned features might not make it into the final release—and some things that do make it in might change between the writing of this book and then—they still give a nice overview of the direction the C# team is taking.

Top-level programs

Code Listing 68 illustrates the code that we have all seen, with a Program class followed by the Main method.

Code Listing 68: Classes implementing IShape interface

using static System.Console;

class Program
{
    static void Main(string[] args)
    {
        var t = new Triangle
        {
            Base = 5.0,
            Height = 10.5
        };

        DisplayArea(t);

        // static local functions
        static void DisplayArea<T>(T shape) where T : IShape
        {
            WriteLine(shape.Area());
        }
    }
}

class Triangle : IShape
{
    public double Height { get; set; }
    public double Base { get; set; }

    public double Area() => Height * Base / 2;
}

class Rectangle : IShape
{
    public double Length { get; set; }
    public double Width { get; set; }

    public double Area() => Length * Width;

}

public interface IShape
{
    double Area();
}

In C# 9, developers will be allowed to simply omit the Program class and the Main method. The code will look as illustrated in Code Listing 69.

Code Listing 69: Omitting the static void Main

using static System.Console;

var t = new Triangle
{
    Base = 5.0,
    Height = 10.5
};

DisplayArea(t);
ReadLine();

// static local functions
static void DisplayArea<T>(T shape) where T : IShape
{
    WriteLine(shape.Area());
}



class Triangle : IShape
{
    public double Height { get; set; }
    public double Base { get; set; }

    public double Area() => Height * Base / 2;
}

class Rectangle : IShape
{
    public double Length { get; set; }
    public double Width { get; set; }

    public double Area() => Length * Width;

}

public interface IShape
{
    double Area();
}

This means you can just write top-level statements at the top of your file. The top-level statements remain part of your Main method, and the DisplayArea local function remains a local function; it’s just called from the top-level statements.

Top-level statements must precede namespaces and type declarations, and can only appear in one file.

Another great feature is that if you place your await statements as top-level statements, then the Main becomes an async Task Main. At this point, I bet you are wondering what will become of the args argument in the Main method.

This is not in any of the previews just yet, but there is a possibility that args will become a magic keyword. This means the C# team will make a variable available in the top-level statements called args. The magic variable makes me think of value in a property setter.

Relational and logical patterns

C# 9.0 will introduce patterns that correspond to relational operators, such as < and <=. You will also be able to combine patterns with logical operators such as and, or, and not. Consider the code in Code Listing 70.

Code Listing 70: Method with switch expression

public AreaSize DoSomething<T>(T shape, int numberOfShapes) where T : IShape
{           
    var area = shape switch
    {
        Square s => s.Area() * numberOfShapes,
        Circle c => c.Area() * numberOfShapes,
        null => throw new ArgumentNullException(nameof(shape)),
        _ => throw new ArgumentException(message: $"Unknown shape: {shape}", paramName: nameof(shape))
    };

    if (area < 3.0)
        return AreaSize.small;
    else if (area < 5.0)
        return AreaSize.medium;
    else if (area < 7.0)
        return AreaSize.large;
    else
        return AreaSize.huge;
}

We have checked for null in the switch expression, and if the shape is null, we throw an ArgumentNullException. We can move the null => to the top of the switch expression if we want to (because if it is null, why carry on?). The position here doesn’t matter, but what is clear is that if we reach the discard _ =>, we know that the shape is not null.

Consider the code in Code Listing 71. At this point in the switch expression, we can be certain that shape is not null.

Code Listing 71: The discard in the switch expression

_ => throw new ArgumentException(message: $"Unknown shape: {shape}", paramName: nameof(shape))

This means we can make our intent clearer by using the not logical operator, as illustrated in Code Listing 72.

Code Listing 72: Using the not logical operator

not null => throw new ArgumentException(message: $"Unknown shape: {shape}", paramName: nameof(shape))

Secondly, because C# 9.0 will add relational patterns, we can modify the if/else statement, as illustrated in Code Listing 73.

Code Listing 73: The if else statement to convert

if (area < 3.0)
    return AreaSize.small;
else if (area < 5.0)
    return AreaSize.medium;
else if (area < 7.0)
    return AreaSize.large;
else
    return AreaSize.huge;

Interestingly enough, Visual Studio 2019 version 16.7.0 Preview 4.0 supports converting this statement to a switch expression using relational patterns, as seen in Figure 10.

Convert to switch expression

Figure 10: Convert to switch expression

The converted switch expression looks nice and succinct, as illustrated in Code Listing 74.

Code Listing 74: A switch expression using a relational pattern

public AreaSize DoSomething<T>(T shape, int numberOfShapes) where T : IShape
{
    var area = shape switch
    {
        Square s => s.Area() * numberOfShapes,
        Circle c => c.Area() * numberOfShapes,
        null => throw new ArgumentNullException(nameof(shape)),
        not null => throw new ArgumentException(message: $"Unknown shape: {shape}", paramName: nameof(shape))
    };
           
    //Relational pattern(min 21)
    return area switch
    {
        < 3.0 => AreaSize.small,
        < 5.0 => AreaSize.medium,
        < 7.0 => AreaSize.large,
        _     => AreaSize.huge
    };
}

Taking logical patterns further, we can now get rid of unwieldy double parentheses for if conditions. Consider the code in Code Listing 75.

Code Listing 75: Standard if not condition

if (!(shape is Circle)) { }

We can now replace this with the code in Code Listing 76.

Code Listing 76: New if not condition using a logical pattern

if (shape is not Circle) { }

If a shape can be a Circle or a Square, we can do the following:

Code Listing 77: Logical or pattern

if (shape is Circle or Square) { }

This will give developers a fantastic way to express the intent of their code clearly and succinctly.

Target-typed new expressions

Before C# 9.0, whenever you wrote a new expression in C#, you were required to specify the type. The only exception was implicitly typed arrays, where you would create the following array:

Code Listing 78: Implicitly typed array

var planets = new[] { "Mars", "Saturn", "Jupiter" };

In C# 9.0, you will be allowed to omit the type when it’s clear what type the expression is being assigned to. Consider the following code.

Code Listing 79: The new expression for creating a Circle

Circle c = new Circle(5);

In C# 9.0, this code can simply be written as follows.

Code Listing 80: The target-typed new expression

Circle c = new (5);

This code looks neater and conveys exactly what it should. I do, however, think that those of us using var will probably not use target-typed new expressions too often.

Init-only properties

C# allows developers to use object initialization, which is a very convenient and flexible way of creating a new object. Consider the SalesOrder class in Code Listing 81. It currently uses an auto-property for OrderNumber.

Code Listing 81: The SalesOrder class

public class SalesOrder
{
    public string OrderNumber { get; set; }
}

One limitation is that the properties have to be mutable for object initializers to work. The object’s default, parameterless constructor is called, and then the property is assigned. But you can set the property to a different value after initialization because it is mutable, as seen in Code Listing 82.

Code Listing 82: Initializing the SalesOrder class

var salesOrder = new SalesOrder
{
    OrderNumber = "123"
};

salesOrder.OrderNumber = "345";

With init-only properties, this mutability is fixed. The init accessor is a variant of the set accessor, and can only be called during object initialization. If you modify your property to use init, as seen in Code Listing 83, then any subsequent assignments to the OrderNumber property will result in a compile-time error.

Code Listing 83: Setting init-only property on SalesOrder class

public class SalesOrder
{
    public string OrderNumber { get; init; }
}

Visual Studio will tell you that your assignment after object initialization is not allowed.

Subsequent assignment results in a compile-time error

Figure 11: Subsequent assignment results in a compile-time error

This is because the OrderNumber property is not mutable.

Init accessors and readonly fields

Consider the same SalesOrder class we had a look at earlier. Modifying it as illustrated in Code Listing 84, you will notice the following.

Code Listing 84: Init accessor on the readonly field

public class SalesOrder
{
    private readonly string orderNumber;
    public string OrderNumber
    {
        get => orderNumber;
        init => orderNumber = (value ?? throw new ArgumentNullException(nameof(OrderNumber)));
    }
}

This is possible because init accessors can only be called during initialization. This means that they can mutate readonly fields in the enclosing class.

Records

In the previous code listings, we saw that we can make individual properties immutable by using the init accessor. As seen in Code Listing 85, we can make the whole SalesOrder class become immutable and behave like a value by adding the data keyword.

Code Listing 85: Creating a record

public data class SalesOrder
{
    public string OrderNumber { get; init; }
}

Adding the data keyword to the class declaration marks the class as a record. This means records are seen more as values, and less as objects—they don’t have a mutable encapsulated state. To represent any change over time, you must create a new record that represents the new state, meaning they are defined by their contents.

More C# 9.0 goodies

These are only some of the planned features for C# 9.0. While I know a lot could change before its release, the code illustrated in the previous examples gives us a nice glimpse of where the C# team is headed. If you want to keep up to date on what is happening around C# 9.0, swing over to the Language Feature Status page on GitHub. This allows you to see planned C# 9.0 features and their current states.

Scroll To Top
Disclaimer

DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.