
.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
- Raw string literals
- UTF-8 string literals
- List patterns
- Newlines in string interpolations
- Auto-default structs
- Pattern match Span
on a constant string - Generic attributes
- Extended nameof scope
- File scoped types
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; }
}