Functional Programming in C#

Learn how to write better C# code

New features and improvements in C# 8.0, C# 9.0 and C# 10.0

Contents

New features and changes in C#

What's new in C# 10.0

C# 10.0 is the current version of the language which was released in November 2021. It is supported in .NET 6.0 and Visual Studio 2022 version 17.0. This iteration adds a lot of new features and improvements to the language:

Record structs

With C# 10.0 we can use record struct to define a value type record. For example:

// positional parameters
public readonly record struct Point(double X, double Y);

// the same example with property syntax
public record struct Point
{
public double Y { get; init; }
public double X { get; init; }
}

More information and examples in the official documentation from Microsoft.

Interpolated string handler

This is an advanced scenario, typically used for performance reasons. In C# we can create interpolated strings such as $"Hi, {name}". This will be transformed by the compiler to a single string instance using a call to string.Format or string.Concat. With this new feature we can change the default behaviour. Read more in the language reference or check out this great example at dontcodetired.com

Constant interpolated strings

We can now use const for interpolated strings if all the placeholders are themselves constant strings.

const string scheme = "https";
const string url = $"{scheme}://localhost:5000";

More examples at daveabrock.com

File-scoped namespace declaration

File-scoped namespace declaration means that all declared types in a file are in a single namespace.

// before C# 10.0 this is how to declare a scope that contains a set of related objects
namespace SimpleProgram
{
class SomeClass { }
struct SomeStruct { }
}

// the following example is similar to the previous one, but uses a file scoped namespace declaration
namespace SimpleProgram;

class SomeClass { }
struct SomeStruct { }

More information in the documentation.

Global using directives

Adding the global keyword to any using directive instructs the compiler that the directive applies to all source files in the compilation.

global using System;

Detailed explanation with examples in dotnetcoretutorials.com

Record types can seal ToString

Adding the sealed modifier when overriding ToString() method in a record type prevents the compiler from synthesizing a ToString method for any derived record types. Learn more in the article on records in docs.microsoft.com.

Assignment and declaration in same deconstruction

In C# 10.0 we can assign a value to existing variable and declare new variable at the time of deconstruction. For example:

int x = 0;
(x, int y) = point;

// in earlier versions

// initialization
(int x, int y) = point;

// assignment
int x1 = 0;
int y1 = 0;
(x1, y1) = point;

Improvements of structure types

struct types can have a parameterless constructor and can initialize an instance field or property at its declaration. Detailed information on docs.microsoft.com

public struct Measurement
{
// Parameterless constructor with property initialization
public Measurement()
{
Value = 123;
}

// Initialization of the property at its declaration
public int Value { get; init; } = 123;
}

Also a left-hand operand of the with expression can be of any structure type or an anonymous (reference) type.

Extended property patterns

C# 10 improved referencing nested properties or fields within a property pattern. For example:

// in C# 10
{ Prop1.Prop2: pattern }

// in C# 8.0 and later
{ Prop1: { Prop2: pattern } }

Read more in code-maze.com

Lambda expression improvements

C# 10 includes many improvements to lambda expressions that make them more similar to methods and local functions. Read more in the official blog post from Microsoft.

Improved definite assignment

The main impact of this improvement is that the warnings for definite assignment and null-state analysis are more accurate. See example in docs.microsoft.com

Allow AsyncMethodBuilder attribute on methods

Read more in docs.microsoft.com

CallerArgumentExpression attribute diagnostics

Learn more about this feature in docs.microsoft.com

Enhanced #line pragma

This feature is tailored towards domain-specific languages (DSLs) like Razor.

What's new in C# 9.0

C# 9.0 was released in November 2020 with .NET 5 and Visual Studio 2019 version 16.8. This version contains the following new and enhanced features:

Record types

We can declare a record type using the record keyword. It is a new reference type that can be used instead of classes and structs. Record instances can have immutable properties as shown in the example below:

// immutable record with positional parameters
public record Person(string FirstName, string LastName);

// immutable record with init only properties
public record Person
{
public string FirstName { get; init; } = default;
public string LastName { get; init; } = default;
};

We can also create record types with mutable properties and fields:

public record Person
{
public string FirstName { get; set; } = default;
public string LastName { get; set; } = default;
};

See full details and more examples in docs.microsoft.com or check this article with key takeaways in infoq.com

Init only setters

C# 9.0 introduced init accessor that allows properties to be assigned once during object initialization. After that they become immutable and can't be changed.

public class Product
{
public string Name { get; init; }
public decimal Price { get; init; }
}

var product = new Product
{
Name = "C# in Depth Book",
Price = 31,
};
product.Price = 35; // this will result in an error

For more information see init reference page.

Top-level statements

All .NET executable programs need an entry point, typically a Main method. This is the place from where the execution of the program starts. Top-level statements removed this requirement.

// before C# 9.0
using System;

namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}

With top-level statements the same code can be written like this:

// C# 9.0 and later
using System;

Console.WriteLine("Hello World!");

For more information see Top-level statements in the C# Programming Guide.

Pattern matching enhancements

C# 9.0 includes new pattern matching improvements. Consider the following examples:

public static bool IsLetter(this char c)
=> c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';

// with optional parentheses to make it clear that 'and' has higher precedence than 'or'
public static bool IsLetterOrSeparator(this char c)
=> c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');

// new syntax for a null check:
if (e is not null)
{
}

Check out the annoncement post for quick reference or for detailed information read patterns article in the C# language guide.

Performance and interop

Three new features improve support for native interop and low-level libraries that require high performance: native sized integers, function pointers, and omitting the localsinit flag. These features can improve performance in some scenarios. They should be used only after careful benchmarking both before and after adoption. Read more in the annoncement post.

Fit and finish features

In C# 9.0 we can skip the type in a new expression when the created object's type is known. For example:

private List<User> users = new();

public User GetUser(int id, UserOptions options);
var user = GetUser(1, new());

Starting in C# 9.0 we can add the static modifier to lambda expressions or anonymous methods.

Read more in the annoncement post.

Support for code generators

Source generator is code that runs during compilation and can inspect your program to produce additional files that are compiled together with the rest of your code. Read more in the introduction post at devblogs.microsoft.com or in this great article in medium.com.

What's new in C# 8.0

C# 8.0 was released in September 2019 and is supported on .NET Core 3.x and .NET Standard 2.1. This version of the language adds the following features and improvements.

Readonly members

This feature allow us to declare members of a struct as read-only. In earlier versions this was only possible at the whole structure level.

public struct Math
{
// auto-implemented getters are read-only by default and the compiler will treat them as read-only.
public int a { get; set; }
public int b { get; set; }

public readonly int sum => (a + b);
}

For more check out this article in riptutorial.com or the official blog post from docs.microsoft.com

Default interface methods

With this feture now its possible to add default implementation for methods to an existing interface without breaking the classes that implement that interface.This means that it is optional for implementers to override the methods or not. More info and key takeaways in infoq.com

interface IWriteLine
{
public void WriteLine()
{
Console.WriteLine("C# 8.0");
}
}

More patterns in more places

With C# 8.0 we can use more pattern expressions in more places.

Shape shape = new Rectangle
{
Width = 100,
Height = 100,
Point = new Point { X = 0, Y = 100 }
};
var result = shape switch
{
Rectangle (100, 100, null) => "Found 100x100 rectangle without a point",
Rectangle (100, 100, _) => "Found 100x100 rectangle",
_ => "Different, or null shape"
};

Read more about patterns in the C# language reference

Switch expressions

This new feature enable us to use more concise expression syntax. There are fewer repetitive case and break keywords, and fewer curly braces. More info here.

public enum Rainbow
{
Red,
Orange,
Yellow
}

public string FromRainbow(Rainbow colorBand)
=> colorBand switch
{
Rainbow.Red => "red",
Rainbow.Orange => "orange",
Rainbow.Yellow => "yellow",
}

Property patterns

Now we can match an expression's properties or fields against nested patterns. Consider the following example:

bool IsConferenceDay(DateTime date)
=> date is { Year: 2020, Month: 5, Day: 19 or 20 or 21 };

Read more in the section for patterns in the C# reference.

Tuple patterns

Tuple patterns allow us to switch based on multiple values expressed as a tuple.

public string RockPaperScissors(string first, string second)
=> (first, second) switch
{
("rock", "paper") => "rock is covered by paper. Paper wins.",
("rock", "scissors") => "rock breaks scissors. Rock wins.",
(_, _) => "tie"
};

Read more here.

Positional pattern

We can use a positional pattern to deconstruct an expression result and match the resulting values against the corresponding nested patterns. Check out examples and more info here.

Using declarations

In C# 8.0 we can use the using keyword infront of a variable declaration. This means that the variable being declared should be disposed at the end of the enclosing scope. Read more here.

using var fileStream = File.OpenRead(@"C:\somefile.txt");

// equivalent to:
using (var fileStream = File.OpenRead(@"C:\somefile.txt"))
{
// statements
}

Static local functions

A local function declared static can't reference state from the enclosing scope. This means that local variables, parameters, and this from the enclosing scope are not available within this function.

int M()
{
int y = 5;
int x = 7;
return Add(x, y);

static int Add(int left, int right) => left + right;
}

Read more in the official documentation.

Disposable ref structs

A ref struct can't implement any interfaces including IDisposable. In this case, to enable a ref struct to be disposed, it must have an accessible void Dispose() method.

Read more here.

Nullable reference types

In C# 8.0 the compiler will help you to identify most of the null reference bugs before they become exceptions at runtime. The goal of nullable reference types is to prevent null reference exceptions. By default, for reasons of backwards compatibility, this feature is not enabled.

public int GetLength(string? str)
{
// warning: CS8602: Dereference of a possibly null reference.
return str.Length;
}

Read more here or in this blog post.

Asynchronous streams

A method that returns an asynchronous stream has three properties:

  1. It's declared with the async modifier.
  2. It returns an IAsyncEnumerable<T>.
  3. The method contains yield return statements to return successive elements in the asynchronous stream.

public static async IAsyncEnumerable<int> GenerateSequence()
{
for (int i = 0; i < 20; i++)
{
await Task.Delay(100);
yield return i;
}
}

await foreach (var number in GenerateSequence())
{
}

For more information check out this tutorial.

Asynchronous disposable

Now the language supports asynchronous disposable types that implement the System.IAsyncDisposable interface. We use the await using statement to work with an asynchronously disposable object. For more information, see the Implement a DisposeAsync method article.