C# 10.0 - Here To Make Coding Simpler

C# 10.0 is going to be out there soon along with the full release of .NET 6.0 and it has got some features this time that is going to make the code writing experience simpler and more aligned to the simplistic form of presentation. This article discusses the features available in C# 10.0 as of .NET 6.0 preview 7 and is due to be released alongside the full release of .NET 6.0 in November 2021.

Let’s go over some of the noteworthy features one by one,

Parameterless constructors in Structure types

While in C# 7.2 Microsoft introduced the ‘readonly’ attribute on structure types that are used to declare them as immutable and in C# 9.0 introduced a new ‘init’ keyword on properties that were also available on structure types. This time with C# 10.0, structure types have got another improvement in the form of the parameterless constructors.

You can declare a parameterless instance constructor in a structure type as the following example shows,

public struct FloorDimension {
    public FloorDimension() {
        Length = double.NaN;
        Breadth = double.NaN;
        UnitOfMeasure = "Undefined";
    }
    public FloorDimension(double length, double breadth, string unitOfMeasure) {
        Length = length;
        Breadth = breadth;
        UnitOfMeasure = unitOfMeasure;
    }
    public double Length {
        get;
        init;
    }
    public double Breadth {
        get;
        init;
    }
    public string UnitOfMeasure {
        get;
        init;
    };
    public override string ToString() => $ "Length: {Length}, Breadth: {Breadth}, Unit: {UnitOfMeasure}";
}
public static void Main(string[] args) {
    var d1 = new FloorDimension();
    Console.WriteLine(d1);
    // prints => Length: NaN, Breadth: NaN, Unit: Undefined
    var d2 =
        default (FloorDimension);
    Console.WriteLine(d2);
    // prints => Length: 0, Breadth: 0, Unit: 
}

Instance field initialization in Structure types during declaration

From C# 10.0 onwards, the instance fields or properties of a structure type can be initialized where they are being declared. Let’s extend the above example to include initializers,

public struct FloorDimension {
    public FloorDimension(double length, double breadth) {
        Length = length;
        Breadth = breadth;
    }
    public FloorDimension(double length, double breadth, string unitOfMeasure) {
        Length = length;
        Breadth = breadth;
        UnitOfMeasure = unitOfMeasure;
    }
    public double Length {
        get;
        init;
    }
    public double Breadth {
        get;
        init;
    }
    public string UnitOfMeasure {
        get;
        init;
    } = "meters";
    public override string ToString() => $ "Length: {Length}, Breadth: {Breadth}, Unit: {UnitOfMeasure}";
}
public static void Main(string[] args) {
    var d1 = new FloorDimension(4, 3);
    Console.WriteLine(d1);
    // prints => Length: 4, Breadth: 3, Unit: meters
}

In the case where a parameterless constructor is not declared explicitly, a structure type provides a parameterless constructor such that,

If a structure type has explicit instance constructors or has no field initializers, an implicit parameterless constructor produces the default value of a structure type, regardless of field initializers. So if we use the above-mentioned structure as an example with a parameterless constructor within the Main method, we have,

public static void Main(string[] args) {
    var d1 = new FloorDimension();
    Console.WriteLine(d1);
    // prints => Length: 0, Breadth: 0, Unit: 
}

If a structure type has no explicit instance constructors and has field initializers, the compiler synthesizes a public parameterless constructor that performs the specified field initializations. This can be illustrated with the below example,

public struct FloorDimension {
    public double Length = double.NaN
    public double Breadth = double.NaN
    public string UnitOfMeasure = "meters";
    public override string ToString() => $ "Length: {Length}, Breadth: {Breadth}, Unit: {UnitOfMeasure}";
}
public static void Main(string[] args) {
    var d1 = new FloorDimension();
    Console.WriteLine(d1);
    // prints => Length: NaN, Breadth: NaN, Unit: meters
    var d2 =
        default (FloorDimension);
    Console.WriteLine(d2);
    // prints => Length: 0, Breadth: 0, Unit: 
}

Declaration of using directives with global modifier

From C# 10.0 onwards, we can add the global modifier to any using directive. This way the directive gets applied to all the source files in a project. Using this feature we can now declare all the using directives at one place in a project instead of declaring them in each file or namespace in a project. 

Till now we have been declaring using directives in all the files of a project in the below manner,

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

Using the ‘global’ keyword we can declare the same in just anyone file in the project as follows,

global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading.Tasks;

File-scoped namespace declaration

We have been used to a namespace declaration where the types within a namespace are scoped using a pair of braces.

using System;
namespace SimpleNamespace {
    // type declarations
    namespace NestedNamespace {
        // type declarations
    }
}

However, with C# 10.0, a new form of the namespace declaration is being introduced that is scoped to a file and doesn’t have the braces. Any declarations made below the namespace in a file are part of the namespace.

using System;
namespace FileScopedNamespace;
// type declarations

As you would notice from the difference in the two examples taken, it is simpler to write a file-scoped namespace declaration and it reduces one level of indentation. 

It comes with a catch though. When using a file-scoped namespace declaration, it is not possible to have nested namespaces or other file-scoped namespace declarations subsequently in the same file. For example-

using System;
namespace FileScopedNamespace;
// type declarations
namespace FileScopedNamespace2; // not possible
namespace NestedNamespace // not possible
{
    // type declarations...
}

Reference nested properties within property patterns

Property patterns were introduced in C# 8.0 and are used to match an expression's properties or fields against nested patterns. An example would be,

public record House(ushort DoorNumber, string BuildingName);
public record Locality(string Street, string City);
public record Address(House HouseInfo, Locality LocalityInfo)
static bool IsMyAddress(Address address) => address is {
    HouseInfo: {
        Door: 123,
        BuildingName: "AB Apartments"
    },
    LocalityInfo: {
        Street: "CD Street",
        City: "XYZ"
    }
};

In C# 10.0, we can reference nested properties or fields within a property pattern. It simplifies the whole pattern declaration and makes it more readable. Using extended property patterns, the above example can be simplified as follows,

public record House(ushort DoorNumber, string BuildingName);
public record Locality(string Street, string City);
public record Address(House HouseInfo, Locality LocalityInfo)
static bool IsMyAddress(Address address) => address is {
    HouseInfo.Door: 123, HouseInfo.BuildingName: "AB Apartments",
        LocalityInfo.Street: "CD Street", LocalityInfo.City: "XYZ"
};

Use one or more constant strings while declaring another constant string using interpolation

Starting C# 10.0 onwards, we can initialize const strings that are a combination of other const strings using string interpolation. This way we can initialize smaller reusable constant strings and use them to form a bigger and variety of constant strings.

For example,

const string scheme = "https://";
const string csharpCornerHost = "c-sharpcorner.com";
const string technologiesPath = "/technologies";
const string learnPath = "/learn";

const string csharpCornerBaseUrl = $"{scheme}{csharpCornerHost}";
const string technologiesUrl = $"{csharpCornerBaseUrl}{technologiesPath}";
const string learnUrl = $"{csharpCornerBaseUrl}{learnPath}";

One thing to keep in mind though is that the placeholder expressions can't be numeric constants because those constants are converted to strings at runtime. The current culture may affect their string representation.

So something like below will not work,

const int defaultLength = 10;
const int defaultBreadth = 5;
const string unitOfMeasure = "meter";
const string defaultRoomDimension = $"Length: {defaultLength}, Breadth: {defaultBreadth}, Unit: {unitOfMeasure}";   // Not possible

So these are some of the noteworthy features going to be available in C# 10.0 as of .NET 6 preview 7. In order to try these out yourself, please download .NET 6.0 (Release Candidate) SDK that is available on Microsoft's .NET downloads page or download the Visual Studio 2022 preview which has .NET 6.0 preview SDK bundled in it already.