What's new in C# 11

A feature-by-feature walk through C# 11: file-scoped types, raw string literals, generic attributes, list patterns, required members, and generic math.

  • programming
  • programming languages
  • dotnet
A row of joined language blocks with a new block descending into an empty slot

C# 11 ships with .NET 7 this autumn, and the release notes hide more good material than a version bump usually earns. Some of it is small comfort, some of it closes requests that sat open for a decade. Here is the tour, feature by feature, with runnable examples along the way.

File-scoped types

C# already carries a full shelf of visibility modifiers: private, protected, public, the slightly puzzling protected internal, and the assembly-level internal. An assembly is the unit .NET has always organized visibility around — but sometimes the assembly is still too wide a room, and what you actually want to scope by is one source file.

That is what the new file modifier does: a type marked with it exists for its own file and nowhere else. The stated motivation is narrow — source generators emitting types without fear of name collisions — but modifiers have a habit of finding second lives once they exist.

Raw string literals

Anyone who has touched Python will recognize the shape immediately:

Raw string literals

C#

string sonnet129 = """
	Is lust in action: and till action, lust
	Is perjured, murderous, bloody, full of "blame",
	Savage, extreme, rude, cruel, not to trust;
	Enjoyed no sooner but despised straight;
""";

Multiline text, quotes inside, no escaping anywhere. Interpolation works too — prefix with $$ and write the placeholders with doubled braces:

Interpolation in raw strings

C#

var name = "World";
var greeting = $$"""
	Hello, {{name}}!
""";

Generic attributes

Library authors will get the most out of this one. The old ritual was [YourAttribute(typeof(TheType))] — the user smuggles a type in as a value, and the library digs it back out with reflection. Now the attribute itself can be generic:

Generic attributes

C#

using System;

public class YourAttribute<T> : Attribute {}

[YourAttribute<TheType>]
public class TheType {}

List patterns

Pattern matching came to C# from the functional world, and the language keeps absorbing more of it release by release. The established form matches on properties:

Property patterns, since C# 8

C#

using System;

var order = new MyOrder(7, 750);
Console.WriteLine(GetDiscount(order));

decimal GetDiscount(MyOrder o) =>
    o switch
    {
        { Number: > 11, Price: > 1001.00m } => 0.1m,
        { Number: > 6, Price: > 501.00m } => 0.050m,
        { Price: > 251.00m } => 0.020m,
        null => throw new ArgumentNullException(nameof(o), "Can't get a discount"),
        _ => 0.0m,
    };

record MyOrder(int Number, decimal Price);

Output

0.050

C# 11 extends the same machinery to sequences:

List patterns

C#

using System;
using System.Collections.Generic;

var list = new List<int> { 1, 2, 3 };
Console.WriteLine(Cases(list));

int Cases(List<int> list)
    => list switch
    {
            [1, _, 3] => 1, // [1, 2, 3] or [1, 1, 3] ...
            [0, ..]   => 2, // [0, 1, 2] or [0, 5] or [0] ...
            [1, ..]   => 3, // [1, -1, 2] or [1, 5, 3] ...
            []        => 4, // only empty

            // handle all of the other cases:
            _  => throw new ArgumentOutOfRangeException($"{list}")
    };

Output

1

The vocabulary is compact: .. swallows zero or more elements, _ stands for exactly one, [] is the empty list, and a literal list matches itself.

Required members

This release adds one of the reachable kind. The question it answers is old: how do you declare that a member of a class, struct, or record must be initialized — no constructor gymnastics, no runtime null surprises? The answer is now a keyword, required:

Required members

C#

using System;

var address = new Address { FirstLine = "Wall st. 57", PostCode = 22101 };
Console.WriteLine(address);

class Address
{
    public required string FirstLine { get; init; }
    public string? SecondLine { get; init; }
    public string? ThirdLine { get; init; }
    public required decimal PostCode { get; init; }

    public override string ToString() => $"{FirstLine}:{SecondLine}:{ThirdLine}:{PostCode}";
}

Output

Wall st. 57:::22101

Leave out FirstLine or PostCode and the program simply does not compile. Initialization can happen in a constructor or in the object-initializer syntax above — but it has to happen.

Generic math support

The enabling piece is static abstract members in interfaces.

Static abstract

A static abstract member behaves like a regular interface member with one twist: implementers satisfy it with a static member. Operators are static in C#, so this is exactly the door operators needed to enter interfaces. A type that wants +, for instance, implements System.IAdditionOperators<TSelf, TOther, TResult> — here is the shape of it on a plain integer:

Static abstract in interfaces

C#

public readonly struct Int32
{
	// ...
	static int IAdditionOperators<int, int, int>.operator +(int left, int right)
		=> left + right;
	// ...
}

Every operator in the language can now be demanded by an interface this way. There is also static virtual, which works the same except the interface itself supplies a body — the static cousin of default interface methods.

Generic math usage

The built-in numeric types already implement the new interfaces, so an algorithm can be written once against INumber<T> and run on any of them:

One algorithm, any number

C#

using System;
using System.Numerics;

Console.WriteLine(ComputeAbstractly(1, 2));
Console.WriteLine(ComputeAbstractly(1.5, 2.9));

TNum ComputeAbstractly<TNum>(TNum a, TNum b) where TNum : INumber<TNum>
{
    return a + b * a;
}

Output

3
5.85

Your own types can join in by implementing INumber<T> — though it is a wide interface, so for a taste of the mechanics a small IMonoid<T> makes a better demonstration:

A custom monoid

C#

using System;
using System.Numerics;

var a = new CustomInteger(1);
var b = new CustomInteger(2);

Console.WriteLine(a + b);
Console.WriteLine(CustomInteger.Zero);

class CustomInteger : IMonoid<CustomInteger>
{
    int _inner;
    public CustomInteger(int num)
        => _inner = num;
    public override string ToString()
        => "CustomInteger{" + _inner.ToString() + "}";

    public static CustomInteger operator +(CustomInteger a, CustomInteger b)
        => new CustomInteger(a._inner + b._inner);
    public static CustomInteger Zero
        => new CustomInteger(0);
}

Output

CustomInteger{3}
CustomInteger{0}

The .NET blog has the long version of the generic math story.

Ending

A dense release, all told. Christmas arrived a couple of months early this year — my thanks once more to the community and the .NET team, and I would not bet against more of this arriving soon.