C# 11 is coming, so we're going to explore its new features in detail. You may find these new features pretty curious even though there are not that many of them. Today let's take a closer look at the generic math support, raw string literals, the required modifier, the type parameters in attributes, and more.
C# 11 added support for generic attributes — now we can declare them similarly to generic classes and methods. Even though we have been able to pass a type as a parameter in a constructor before, we now can now use the where constraint to specify what types should be passed. Now we also don't have to use the typeof operator all the time.
This is how it works on the example of a simple implementation of the "decorator" pattern. Let's define the generic attribute:
[AttributeUsage(AttributeTargets.Class)]
public class DecorateAttribute<T> : Attribute where T : class
{
public Type DecoratorType{ get; set; }
public DecorateAttribute()
{
DecoratorType = typeof(T);
}
}
Next, we implement a hierarchy (according to the pattern) and a factory to create decorated objects. Pay attention to the Decorate attribute:
public interface IWorker
{
public void Action();
}
public class LoggerDecorator : IWorker
{
private IWorker _worker;
public LoggerDecorator(IWorker worker)
{
_worker = worker;
}
public void Action()
{
Console.WriteLine("Log before");
_worker.Action();
Console.WriteLine("Log after");
}
}
[Decorate<LoggerDecorator>]
public class SimpleWorker : IWorker
{
public void Action()
{
Console.WriteLine("Working..");
}
}
public static class WorkerFactory
{
public static IWorker CreateWorker()
{
IWorker worker = new SimpleWorker();
if (typeof(SimpleWorker)
.GetCustomAttribute<DecorateAttribute<LoggerDecorator>>() != null)
{
worker = new LoggerDecorator(worker);
}
return worker;
}
}
Let's see how it works:
var worker = WorkerFactory.CreateWorker();
worker.Action();
// Log before
// Working..
// Log after
Please note that the limitations have not been fully removed, the type should be specified. For example, you can't use the type parameter of the class:
public class GenericAttribute<T> : Attribute { }
public class GenericClass<T>
{
[GenericAttribute<T>]
//Error CS8968 'T': an attribute type argument cannot use type parameters
public void Action()
{
// ....
}
}
You may ask, are GenericAttribute<int> and GenericAttribute<string> different attributes, or just multiple uses of the same one? Microsoft determined them to be the same attribute. This means, to use the attribute multiple times, you need to set the AllowMultiple property to true. Let's modify the above example:
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class DecorateAttribute<T> : Attribute where T : class
{
// ....
}
Now the object can be decorated several times:
[Decorate<LoggerDecorator>]
[Decorate<TimerDecorator>]
public class SimpleWorker : IWorker
{
// ....
}
Obviously, this new feature is not a fundamental one, but library developers can now create a more user-friendly interface when working with generics in attributes.
In the new version of the C# language, we can use mathematical operations on generic types.
This new feature results in two general consequences:
Now we got a static abstract construct that may seem a bit weird, but I wouldn't call it completely senseless. Let's inspect the actual result of this update. Take a look at this slightly contrived example of the natural number implementation (the number can be added up and parsed from a string):
public interface IAddable<TLeft, TRight, TResult>
where TLeft : IAddable<TLeft, TRight, TResult>
{
static abstract TResult operator +(TLeft left, TRight right);
}
public interface IParsable<T> where T : IParsable<T>
{
static abstract T Parse(string s);
}
public record Natural : IAddable<Natural, Natural, Natural>, IParsable<Natural>
{
public int Value { get; init; } = 0;
public static Natural Parse(string s)
{
return new() { Value = int.Parse(s) };
}
public static Natural operator +(Natural left, Natural right)
{
return new() { Value = left.Value + right.Value };
}
}
We can use the specified operations the following way:
var one = new Natural { Value = 1 };
var two = new Natural { Value = 2 };
var three = one + two;
Console.WriteLine(three);
// Natural { Value = 3 }
var parsed = Natural.Parse("42");
Console.WriteLine(parsed);
// Natural { Value = 42 }
The above example shows that generic interfaces for static abstract methods' deriving should be done through the curiously recurring template pattern of the Natural type: IParsable<Natural>. You have to be careful not to mix up the type parameter — Natural : IParsable<OtherParsableType>.
You can clearly see how static methods work with type parameters in the following example:
public IEnumerable<T> ParseCsvRow<T>(string content) where T : IParsable<T>
=> content.Split(',').Select(T.Parse);
// ....
var row = ParseCsvRow<Natural>("1,5,2");
Console.WriteLine(string.Join(' ', row.Select(x => x.Value)));
// 1 5 2
We couldn't use generics this way before, since, in order to call a static method, we had to specify a type explicitly. Now the method can work with any derivative of IParsable.
Obviously, we can use generics not only to implement math from scratch. The operating mechanism of basic types has also been modified: 20 of basic types implement interfaces corresponding to basic operations. You can read about this in the documentation.
Library support facilitation is described as the main use case of generics. With generics, we can get rid of overloading the same methods to work with all possible data types. For example, here's how you can use the the INumber standard interface to implement an algorithm for summing up numbers from a collection:
static T Sum<T>(IEnumerable<T> values)
where T : INumber<T>
{
T result = T.Zero;
foreach (var value in values)
{
result += T.CreateChecked(value);
}
return result;
}
The method works with both natural and real numbers:
Console.WriteLine(Sum(new int[] { 1, 2, 3, 4, 5 }));
// 15
Console.WriteLine(Sum(new double[] { 0.5, 2.5, 3.0, 4.3, 3.2 }));
// 13.5
And that's not all! The unsigned bitwise shift has also changed. I'm sure that an entire article could be written on this topic. Until then, if you are curious, take a look at the documentation.
Let's sum up all this a little. In fact, not so many people will use these new generics directly, as not everyone develops libraries or performs other tasks in some way related to this feature. For most projects, the static abstract construct may be too peculiar. Nevertheless, the new feature can make our lives better another way – the library developers now will need to spend fewer resources on supporting countless overloads.
C# 11 allows us create raw string literals by repeatedly using quote marks (three or more times). This works similarly to the verbatim identifier (@), except for two important distinctions:
Let's take this method where a json string is formed:
string GetJsonForecast(DateTime date, int temperature, string summary)
=> $$"""
{
"Date": "{{date.ToString("yyyy-MM-dd")}}",
"TemperatureCelsius": "{{temperature}}",
"Summary": "{{summary}}",
"Note": ""
}
""";
The method call returns a usual json string:
{
"Date": "2022-09-16",
"TemperatureCelsius": 10,
"Summary": "Windy",
"Note": ""
}
The main differences between raw string literals and the verbatim identifier (@) are clear: the raw literal is taken "literally" with quotes and braces without any extra whitespace to the left. The syntax rules for "raw" strings are as follows:
Oddly enough, but it seems to be one of the more significant new features. Now we can make multi-line literals without being afraid either for formatting the resulting string or for the cleanness of code. Only one thing remains unclear: why do we still have to use a traditional verbatim identifier when working with text?
Another new feature that helps working with strings. Now the expression inside the interpolation can be moved to a new line:
Console.WriteLine(
$"Roles in {department.Name} department are: {
string.Join(", ", department.Employees
.Where(x => x.Active)
.Select(x => x.Role)
.OrderBy(x => x)
.Distinct())}");
// Roles in Security department are: Administrator, Manager, PowerUser
That's the good news for those who have to place a long query of operators (such as Linq) inside the interpolation. But don't get too caught up in line breaks, now it's also easier to mess it all up:
Console.WriteLine(
$"Employee count is {
department.Employees
.Where(x => x.Active)
.Count()} in the {department.Name} department");
// Employee count is 20 in the Security department
You can add the required modifier to indicate that fields or properties must be initialized inside a constructor or initializer. There are two main reasons for this update.
Firstly, when we work with big class hierarchies, boilerplate code may eventually accumulate. It accumulates because large amount of data is passed to base constructors. Let's take a peek at a typical example with shapes:
class Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
}
// ....
class Textbox : Rectangle
{
public string Text { get; }
public Textbox(int x, int y, int width, int height, string text)
: base(x, y, width, height)
{
Text = text;
}
}
We have omitted some intermediate steps for the sake of brevity, but the problem is still clear: the number of parameters has more than doubled. The example is contrived, but in domain the situation may be even worse (especially if it's complicated by poor architecture).
However, if we make a constructor without parameters and add the required modifier to the X and Y properties, then it changes who is to initialize the type from the developer to its user. Let's modify the above example:
class Point
{
public required int X { get; set; }
public required int Y { get; set; }
}
// ....
class Textbox : Rectangle
{
public required string Text { get; set; }
}
The guarantee of initialization is the very fact of derivation. Now the client is responsible for the initialization:
var textbox = new Textbox() { Text = "button" };
// Error CS9035: Required member 'Point.X' be set in the object initializer
// or attribute constructor
// Error CS9035: Required member 'Point.Y' must be set in the object initializer
// or attribute constructor
// ....
Secondly, it may come in handy when using ORM that require constructors without parameters. After all, you couldn't previously oblige a client to initialize a field with Id.
You can find more details on it in the documentation, but here are the most interesting ones:
Although this feature seems useful, there are still some moot points. First of all, the simultaneous presence of the required modifier and the Required attribute (they perform different tasks) may be confusing for newcomers. Then, we can easily make a mistake when using the SetsRequiredMembers attribute for constructors, since no one can assure that a developer didn't forget to initialize something in the constructor when adding this attribute. For example, if we forget about the parent mandatory constructor (and if the parent happens to have an empty constructor too), then we may write the following code:
class Textbox : Rectangle
{
public required string Text { get; set; }
[SetsRequiredMembers]
public Textbox(string text)
{
Text = text;
}
public override string ToString()
{
return
$"{{ X = {X}, Y = {Y}, W = {Width}, H = {Height}, Text = {Text}} }";
}
}
Ta-da! It compiles! And even works:
var textbox = new Textbox("Lorem ipsum dolor sit.");
Console.WriteLine(textbox);
// { X = 0, Y = 0, Width = 0, Height = 0, Text = Lorem ipsum dolor sit. }
If there are some reference types among the properties, then a warning about a potential null value is issued. But in this case, it's not. The properties are just initialized with default values. And that's not the only scenario – we may forget to add a field to the constructor in the class in which the field was declared. Anyway, I think the feature is a potential pitfall.
Another feature related to initialization. Now we don't have to initialize all structure members in structure constructors. Just as in the case of classes, they are now initialized with default values when a structure instance is created:
struct Particle
{
public int X { get; set; }
public int Y { get; set; }
public double Angle { get; set; }
public int Speed { get; set; }
public Particle(int x, int y)
{
X = x;
Y = y;
}
public override string ToString()
{
return
$"{{ X = {X}, Y = {Y}, Angle = {Angle}, Speed = {Speed} }}";
}
}
// ....
var particle = new Particle();
Console.WriteLine(particle);
// { X = 0, Y = 0, Angle = 0, Speed = 0 }
Sure, this feature is not a major one, but it was actually needed to improve the internal mechanisms of the language (for example, to implement semi-auto properties in the future).
One more enhancement of pattern matching. Now you can use these features for lists or arrays:
We can use all these new features to check the collection using the is operator:
var collection = new int[] { 0, 2, 10, 5, 4 };
if (collection is [.., > 5, _, var even] && even % 2 == 0)
{
Console.WriteLine(even);
// 4
}
In this case, we checked whether the third element is greater than 5, and whether the fifth element is an even number.
Obviously, list patterns can also be used in switch expressions. Capturing the elements from switch expressions is even more fascinating. For example, now you can have fun and write a palindrome check function in the following way:
bool IsPalindrome (string str) => str switch
{
[] => true,
[_] => true,
[char first, .. string middle, char last]
=> first == last ? IsPalindrome (middle) : false
};
// ....
Console.WriteLine(IsPalindrome("civic"));
// True
Console.WriteLine(IsPalindrome("civil"));
// False
Unfortunately, two-dimensional arrays are not supported in this update.
I'm not sure if this new feature could be commonly used, but it is quite curious, at least.
The nameof operator has been slightly enhanced, it means that it can capture the name of a parameter in an attribute on the method or parameter declaration:
[ScopedParameter(nameof(parameter))]
void Method(string parameter)
// ....
[ScopedParameter(nameof(T))]
void Method<T>()
// ....
void Method([ScopedParameter(nameof(parameter))] int parameter)
Actually, this new feature is quite useful for nullable analysis. Now we don't need to rely on strings when eliminating warnings.
[return: NotNullIfNotNull(nameof(path))]
public string? GetEndpoint(string? path)
=> !string.IsNullOrEmpty(path) ? BaseUrl + path : null;
Unnecessary null value warnings are not issued when we use the result of calling the method:
var url = GetEndpoint("api/monitoring");
var data = await RequestData(url);
Another keyword was added. Now we can create a type whose visibility is scoped to the source file in which it is declared. The new feature meets the needs of code generation to avoid naming collisions.
Now, if you declare classes in different files (but in the same namespace), add modifier file to the first one:
// Generated.cs
file class Canvas
{
public void Render()
{
// ....
}
}
And declare the second as usual:
// Canvas.cs
public class Canvas
{
public void Draw()
{
// ....
}
}
Then no conflicts arise. Obviously, nobody limits to use this feature, so it is now possible to make some kind of private classes but without making them nested. If you are wondering why not simply allow to use the private modifier, it's not that difficult. Since the visibility is scoped to the source file (and not to the namespace), then this decision eliminates possible confusion.
C# 11 contains a few other minor changes, which I still should mention:
That's what C# 11 is like. Obviously, not everyone is equally excited about the new features of C# 11. Some of them are overly specific, others are not that significant, while some will probably provide a basis for further language development. However, the new C# release surely brought some useful features. In my opinion, these include the raw string literals and the required initialization. Already know which feature you prefer?
You can learn more about C# 11 in Microsoft documentation.
Have you already read about the features of the previous C# versions? You can read our articles about C# 9 and C# 10 here:
You can also follow me on Twitter.
0