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.
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.050C# 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
1The 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:::22101Leave 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.85Your 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.