Said's Blog

C# 11 - Features

December 11, 20225 min read
image

.NET 7 was released recently alongside with C# 11. In this article, we will take a look at some of the important features of C# 11.

Required members

The required modifier can be used in a property member to ensure that we explictly set a value when an object is initalized. If an object is initialized with a missing required member, we will get a compilation error.

It is also possible to set a required member inside the object's constructor. However, we need to set the [SetsRequiredMembers] attribute above the constructor. This tells the compiler that we are setting the required members inside the constructor.

						public class Product
{
    public Guid Id { get; set; } = Guid.NewGuid();
    public required string Name { get; set; }
    public required double Price { get; set; }
	
    public Product() {}

    [SetsRequiredMembers]
    public Product(string name, double price)
    {
        Name = name;
        Price = price;
    }
}

// Initializations with required properties - valid
var p1 = new Product { Name = "T-shirt", Price = 9.99 };
Product p2 = new("T-shirt", 9.99);

// Initializations with missing required properties - compilation error
var p3 = new Product { Name = "T-shirt" };
Product p4 = new();
					

Raw string literals

C# 11 simplified writting strings that contain quotes, or referencing code snippets like JSON. The format is at least 3 double quotes """..""". If the text contains 3 double quotes, we should use 4 double quotes to escape them.

The interpolation is also possible with the $ sign. The number of $ signs that are prepended to a string represents the number of curly braces required to reference a variable.

						string name = "Said";
int age = 31;
string myJsonString = 
    $$"""
    {
        "Name": {{name}},
        "Age": {{age}}
    }
    """;
					

UTF-8 string literals

With C# 11, we can specify UTF-8 character encoding by adding the u8 suffix on a string.

UTF-8 literals are stored as ReadOnlySpan<byte>. To get an array of bytes we need to use ReadOnlySpan<T>.ToArray() method.

						// C# 10
byte[] array = Encoding.UTF8.GetBytes("Some text");

// C# 11
ReadOnlySpan span = "Some text"u8;
byte[] array = span.ToArray();
					

List patterns

List patterns allow pattern matching for elements in an array, or in a list. It can be used with any pattern, including constant, type, property, and relational patterns.

						var numbers = new[] { 1, 2, 3, 4 };

// List and constant patterns
Console.WriteLine(numbers is [1, 2, 3, 4]); // True
Console.WriteLine(numbers is [1, 2, 4]);    // False

// List and discard patterns
Console.WriteLine(numbers is [_, 2, _, 4]); // True
Console.WriteLine(numbers is [.., 3, _]);   // True

// List and logical patterns
Console.WriteLine(numbers is [_, >= 2, _, _]); // True
					

Newlines in string interpolations

The text inside the { and } characters for a string interpolation can now span multiple lines. This feature makes it easier to read string interpolations that use longer C# expressions, like pattern matching switch expressions, or LINQ queries.

						// switch expression in string interpolation
int yearsXp = 7;
string level = $"The level is {yearsXp switch
{
    1 or 2 or 3 => "beginner",
    > 3 and < 6 => "intermediate",
    > 6 => "expert",
    _ => "unknown",
}}.";
Console.WriteLine(level); // The level is expert.

// LINQ query in string interpolation
int[] numbers = new int[] { 1, 2, 3, 4, 5, 6 };
string message = $"The reversed odd values of {nameof(numbers)} are {string.Join(", ", numbers
			.Where(n => n % 2 == 1)
			.Reverse())}.";
Console.WriteLine(message);
// The reversed even values of numbers are 5, 3, 1.
					

Auto-default structs

In C# 10, we had to explicitly set the default values for each of its members if we include a constructor in a struct. But with C# 11, the compiler automatically initializes any field or property.

						//This code doesn't compile in the previous versions of C#. 
//But now the compiler sets the default values.
struct Product
{
    public Product(string name)
    {
        Name = name;
    }

    public string Name { get; set; }
    public double Price { get; set; }
}
					

Pattern match Span<char> on a constant string

Pattern matching allows to test if a string has a specific constant value. Now, the same pattern matching logic can be used with variables that are Span<char> or ReadOnlySpan<char>.

						ReadOnlySpan<char> str = "Said".AsSpan();
if (str is "Said")
{
    Console.WriteLine("Hi, Said");
}
					

Generic attributes

In C# 10, in order to pass the type to an attribute, we had to use the typeof expression. C# 11 allows generic attributes.

						// Before C# 11:
public class TypeAttribute : Attribute
{
    public Type ParamType { get; }

    public TypeAttribute(Type t)
    {
        ParamType = t;
    }  
}

public class GenericType<T>
{
   [TypeAttribute(typeof(string))]
   public string Method() => default;
}

//-------------------------------------------

//In C# 11
public class GenericAttribute<T> : Attribute { }

public class GenericType<T>
{
   [GenericAttribute()]
   public string Method() => default;
}
					

NB: Type parameters must be supplied when we applying the attribute. In other words, the generic type must be fully constructed.

						public class GenericType<T>
{
   [GenericAttribute<T>()] // Not allowed! generic attributes must be fully constructed types.
   public string Method() => default;
}
					

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 you can use the nameof operator to specify the name of a method parameter in an attribute on the method or parameter declaration.

						public class NameAttribute : Attribute
{
    private readonly string _paramName;
    public NameAttribute(string paramName) => _paramName = paramName;
}

public class MyClass
{
    [Name(nameof(param))]
    public void Method(int param)
    { 
        [Name(nameof(T))] 
        void LocalFunction<T>(T param) { }
		
        var lambdaExpression = ([Name(nameof(aNumber))] int aNumber) => aNumber.ToString();
    }
}

					

File scoped types

A new access modifier file was introduced in C# 11. The visibility of created type is scoped to the source file in which it is declared. This feature helps source generator authors avoid naming collisions.

						file class Product
{
    public string Name { get; set; }
    public double Price { get; set; }
}