Think about the new features in C# 11 as organized around a small set of themes: improved developer productivity, object initialization and creation, generic math support, and runtime performance. You'll benefit from all these features, even if you'll likely use the features in the first two themes far more often than the last two themes. This article discusses the reasons for the new features, provides examples of when you'll use them, and points to the benefits your programs get because the .NET runtime makes use of these features.

Improved Developer Productivity

C# 11 adds many features that improve your productivity. You'll use these features to write more concise and more readable code:

  • Raw string literals
  • Newlines in string interpolations
  • UTF-8 string literals
  • Pattern match Span<char> or ReadOnlySpan<char> on a constant struct
  • List patterns

Let's start with making strings easier to manipulate. Raw string literals provide new syntax, making it easier to embed arbitrary text, including whitespace, new lines, embedded quotes, and other special characters without requiring escape sequences. A raw string literal starts with at least three double-quote (""") characters. It ends with the same number of double-quote characters. Typically, a raw string literal uses three double quotes on a single line to start the string, and three double quotes on a separate line to end the string. The newlines following the opening quotes and preceding the closing quotes aren't included in the final content:

string longMessage = """
    This is a long message.
    It has several lines.
        Some are indented
            more than others.
    Some should start at the first column.
    Some have "quoted text" in them.
    """;

Raw string literals provide new syntax, making it easier to embed arbitrary text, including whitespace, new lines, embedded quotes, and other special characters, without requiring escape sequences.

Any whitespace to the left of the closing triple quotes will be removed from the string literal. Raw string literals can be combined with string interpolation to include braces in the output text. Multiple $ characters denote how many consecutive braces start and end the interpolation:

var location = $$"""
    You are at {{{Longitude}}, {{Latitude}}}
    """;

The preceding example specifies that two braces start and end an interpolation. The third repeated opening and closing brace are included in the output string.

You can use multiple $ characters in an interpolated raw string literal to embed { and } characters in the output string without escaping them. The following snippet shows an example where $$ indicates that two {{ and }} characters open and close an interpolated expression. The third consecutive { or } is added to the output string.

int X = 2;
int Y = 3;
var pointMessage = $$"""
    The point {{{X}}, {{Y}}} is {{Math.Sqrt(X 
                                            * X + Y * Y)}} from the origin
    """;
Console.WriteLine(pointMessage);
// output:
// The point {2, 3} is 3.60555 from the origin.

The preceding example also demonstrates newlines in interpolated strings. The C# expressions embedded in an interpolated string can span multiple source code lines. You can format lengthy expressions such as LINQ queries or pattern matching switch expressions directly inside a string interpolation:

string message = $"The usage policy for {safetyScore} is {safetyScore switch
    {
        > 90 => "Unlimited usage",
        > 80 => "General usage, with safety check",
        > 70 => "Issues must be addressed < 1 week",
        > 50 => "Issues must be addressed today",
        _ => "Issues must be addressed now",
    }
}";

Think of all the code you write where you format strings that include quote characters: LINQ query output, XML output, JSON output, and more. The new raw string literals will make it much easier to format these strings in a way that's easier for developers to read.

Developers who work directly with web standards or other data protocols that use UTF-8 string will appreciate UTF-8 string literals. Add the u8 suffix on a string literal, and the compiler interprets it as a UTF-8-encoded string. The string literal is stored as a ReadOnlySpan<byte>. The UTF-8 string literal can be used with .NET library APIs that require UTF-8 encoded strings. This feature creates a natural syntax for working with UTF-8 strings, providing more readable and more performant code constructs when you use UTF-8 strings.

Pattern matching expressions get an improvement for working with strings and string literals. You can now pattern-match a Span<char> or ReadOnlySpan<char> against a string literal. The compiler generates code to perform the match test without allocating and copying the span or the string.

Finally, List patterns extend pattern matching syntax to match the sequences of elements in a list or an array. Any pattern can be applied to any element in the list to check whether an individual element matches certain characteristics. The discard pattern (_) matches any single element. The range pattern (..) matches zero or more elements in the sequence. At most, one range pattern is allowed in a list pattern. The var pattern can capture any single element, or a range of elements. You can see several examples of list patterns in Listing 1.

Listing 1: List pattern examples

int[] one = { 1 };
int[] odd = { 1, 3, 5 };
int[] even = { 2, 4, 6 };
int[] fib = { 1, 1, 2, 3, 5 };

// You can match the entire sequence by specifying
// all the elements and using values:
Console.WriteLine(odd is [1, 3, 5]);   // true
Console.WriteLine(even is [1, 3, 5]);  // false (values)
Console.WriteLine(one is [1, 3, 5]);   // false (length)

// You can match some elements in a sequence of
// a known length using the discard pattern (_) as a placeholder:

Console.WriteLine(odd is [1, _, _]);   // true
Console.WriteLine(odd is [_, 3, _]);   // true
Console.WriteLine(even is [_, _, 5]);  // false (last value)

// You can supply any number of values or placeholders
// anywhere in the sequence. If you aren't concerned with the length,
// you can use the range pattern to match zero or more elements:
Console.WriteLine(odd is [1, .., 3, _]); // true
Console.WriteLine(fib is [1, .., 3, _]); // true

Console.WriteLine(odd is [1, _, 5, ..]); // true
Console.WriteLine(fib is [1, _, 5, ..]); // false


// The previous examples used the constant pattern
// to determine if an element is a given number.
// Any of those patterns could be replaced by a
// different pattern, such as a relational pattern:
Console.WriteLine(odd is [_, > 1, ..]); // true
Console.WriteLine(even is [_, > 1, ..]); // true
Console.WriteLine(fib is [_, > 1, ..]); // false

List patterns provide a rich syntax to text the shape of sequences to determine whether a sequence contains elements that match required traits, and to ensure that those elements are in the proper location.

You'll often use these new features to write code that is more expressive, more concise, and more understandable. The new syntax makes working with strings and related data structures easier.

Object Initialization and Literals

Another goal for C# is to make it easier to initialize new objects or values correctly. These features enable using consistent syntax with both class types and struct types. Furthermore, when you make mistakes, the compiler surfaces the errors at the location where you can best correct the error. The features added for this goal are:

  • Required members
  • Auto-default struct
  • Extended nameof scope
  • Generic attributes

Required members lets you annotate a member declaration to inform the compiler that it must be initialized either in a constructor or an object initializer. Consider this class:

public class Person
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
}

Callers are expected to set the values for the FirstName and LastName properties using object initializers. However, the compiler doesn't enforce that expectation. In C# 11, you add the required modifier to both property declaration to mandate that callers must initialize these properties:

public class Person
{
    public required string FirstName { get; init; }
    public required string LastName { get; init; }
}

All callers must include object initializers for both properties. Otherwise, the compiler emits an error. The caller must make their code match the expectations set by the type author. If the type also has a constructor that sets the required properties, the type author adds the SetsRequiredMembers attribute to the constructor declaration. Then, the compiler forces that constructor to set an initial value for all required members. Callers using that constructor aren't required to add object initializers for those members. For example, the Person type might have the following declaration:

public class Person
{
    public required string FirstName { get; init; }
    public required string LastName { get; init; }

    public Person() { }

    [SetsRequiredMembers]
    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }
}

Callers using the first constructor must set the required members. Callers using the second constructor rely on the constructor to set the required members.

Auto-default structs clarify the rules for definite assignment in struct constructors. The .NET runtime always initializes the storage for a struct to all 0s. Therefore, struct constructors don't need to set any member to the 0 value explicitly. The additional assignment is unnecessary. Constructors that don't explicitly set all member values now compile, and the compiler sets all members to definitely assigned. In general, when you create a struct by calling a constructor, that constructor initializes all values correctly. When you set a struct to the default value, all struct members are set to 0.

With extended nameof scope, type parameter names and parameter names are now in scope when used in a nameof expression in an attribute declaration on that method. This feature means that you can use the nameof operator to specify the name of a method parameter in an attribute on the method or parameter declaration. This feature is most often useful to add attributes for nullable analysis.

C# 11 adds generic attributes. A generic class can now declare System.Attribute as a based class. This provides a more convenient syntax for attributes that require a System.Type parameter.

These features make it easier for you to initialize objects correctly. They help the developers using your types to use them correctly.

Generic Math Support

The runtime support for generic math is covered in depth elsewhere. There are several new C# features that support this initiative:

  • Static abstract and static virtual members in interfaces
  • Checked user-defined operators
  • Relaxed shift operators
  • Unsigned right-shift operator
  • Numeric IntPtr and UIntPtr

The largest set of changes are those necessary for static abstract and static virtual members in interfaces. The concept is easy to understand: You can declare operators or other static function as virtual or abstract in an interface. Any class that implements that interface must provide an implementation of those operators and static methods. Because these methods are static, the compiler must resolve the target method; there's no runtime dispatch as there is with instance virtual methods. That means that the compiler must be able to determine the correct type for each static method call.

In practice, that means that these interfaces are generally generic interfaces. Furthermore, one of the type parameters must be constrained to be a type that implements the interface. For example, the INumber interface covered in the generic math article declares this constraint, among many others:

public interface INumber<TSelf> where TSelf : INumber<TSelf>

Checked user defined operators enables developers to write different implementations of many overloaded operators for a checked and an unchecked context. In previous versions of C#, all overloaded operators used the same implementation in both a checked and unchecked context. This feature enables type authors to specify different behavior in each context.

The shift operators use to require that the right operand was an int. With relaxed shift operators, that restriction is now removed. Generic math algorithms can use the shift operators, and the right operand can be the type implementing the INumber interface.

The unsigned right shift operator, >>>, shifts an integral type to the right, always inserting 0 in the left-most bits. The arithmetic shift operator, >>, inserts 0 for positive numbers and 1 for negative numbers.

Finally, the keywords nint, and nuint are synonyms for the types System.IntPtr and System.UIntPtr, respectively. The native sized integer keywords were added in C# 10, but were not considered the same as the corresponding types. Now they are.

If you write types that represent numbers, you'll use these features and the corresponding generic math interfaces often. Otherwise, they may not directly affect your code, but you'll benefit from the consolidation of many numeric methods in the runtime library.

Runtime Performance

Finally, C# 11 adds features that can improve runtime performance. Most of these were requested by the .NET Runtime team. They are used in the runtime, so you'll get the performance benefits even if you don't use the feature in your code:

  • Ref fields and scoped ref
  • File local types
  • Cached method group conversion to delegate

Ref fields and scoped ref provide more syntax to enable passing parameters by reference. These features can reduce copying values or allocating new objects. Ref fields allows the ref modifier on a member of a ref struct. This feature minimizes copying when a ref struct needs to reference some storage in another object. The compiler uses static analysis to ensure that the ref struct doesn't have a lifetime that could extend beyond the source of the ref field. Ref parameters can include the scoped modifier to inform the compiler that the reference can't have a lifetime beyond the current method. That restricts how it can be passed, stored, or what storage could be assigned to the parameter. These language enhancements enable developers to write more performant code without using unsafe features. The compiler can enforce lifetime rules of reference variables.

File local types are classes where the definition includes the file modifier. These types can only be accessed within the same source file. They are primarily useful for code generators. A code generator can generate a class scoped to its output source file safely knowing that the type won't conflict with another type in the destination program.

Cached method group conversion to delegate is a compiler performance improvement that you'll benefit from without changing any of your code. This feature benefits from the updated work by the ECMA C# committee. Earlier standards forced the compiler to allocate a new delegate object when code converted a method group to a delegate. The committee updated the standard for version 6 to allow the compiler to cache the delegate object. C# 11, the next compiler released following that change, takes advantage of this new option. Your code makes fewer allocations just by compiling with C# 11.

Wrapping up

C# 11 adds quite a few features, some you're more likely to use than others. Some are for specific scenarios, like generic math; others are more general, like raw string literals. C# keeps evolving to support the applications that you're building today, while still being the language you know and love. You can find links to more details on each of these features at https://docs.microsoft.com/dotnet/csharp/whats-new/csharp-11.