头图

.NET 7 development is just over a month away from entering RC, and new features and improvements for C# 11 are being finalized. At this point in time, many new features have been implemented and merged into the master branch. C# 11 contains a lot of new features and improvements, and the type system has been greatly enhanced compared to before, which greatly improves the language expressiveness while ensuring static type safety. Then this article will be introduced from 5 categories according to the direction, and let's take a look at the new features and improvements of C# 11 in advance.

Type system improvements

▌Abstract and virtual static methods

C# 11 began to introduce abstract and virtual into static methods, allowing developers to write abstract and virtual static methods in interfaces.
Interfaces are different from abstract classes. Interfaces are used to abstract behavior and implement polymorphism through different types of interfaces. Abstract classes have their own state and implement polymorphism by inheriting parent types from subtypes. These are two different paradigms.
In C# 11, the concept of virtual static methods was introduced, and it is possible to write abstract and virtual static methods in interfaces.

 interface IFoo{    // 抽象静态方法
    abstract static int Foo1();    // 虚静态方法
    virtual static int Foo2()
    {        return 42;
    }
}struct Bar : IFoo
{    // 隐式实现接口方法
    public static int Foo1()
    {        return 7;
    }
}

Bar.Foo1(); // ok

Since operators are also static methods, starting with C# 11, operators can also be abstracted with interfaces.

 interface ICanAdd<T> where T : ICanAdd<T>
{    abstract static T operator +(T left, T right);
}

In this way, we can implement this interface for our own types, such as implementing a two-dimensional point Point:

 record struct Point(int X, int Y) : ICanAdd<Point>
{    // 隐式实现接口方法
    public static Point operator +(Point left, Point right)
    {        return new Point(left.X + right.X, left.Y + right.Y);
    }
}

Then we can add the two Points:

 var p1 = new Point(1, 2);var p2 = new Point(2, 3);
Console.WriteLine(p1 + p2); // Point { X = 3, Y = 5 }

In addition to implementing an interface implicitly, we can also implement an interface explicitly:

 record struct Point(int X, int Y) : ICanAdd<Point>
{    // 显式实现接口方法
    static Point ICanAdd<Point>.operator +(Point left, Point right)
    {        return new Point(left.X + right.X, left.Y + right.Y);
    }
}

However, with the explicit implementation of the interface, the + operator is not exposed to the type Point publicly through the public, so we need to call the + operator through the interface, which can be done using generic constraints:

 var p1 = new Point(1, 2);var p2 = new Point(2, 3);
Console.WriteLine(Add(p1, p2)); // Point { X = 3, Y = 5 }T Add<T>(T left, T right) where T : ICanAdd<T>{    return left + right;
}

For cases that are not operators, abstract and static methods on interfaces can be called with generic parameters:

 void CallFoo1<T>() where T : IFoo{
    T.Foo1();
}

Bar.Foo1(); // errorCallFoo<Bar>(); // okstruct Bar : IFoo
{    // 显式实现接口方法
    static void IFoo.Foo1()
    {        return 7;
    }
}

Also, an interface can be extended based on another interface, so for abstract and virtual static methods, we can take advantage of this feature to implement polymorphism on an interface.

 CallFoo<Bar1>(); // 5 5CallFoo<Bar2>(); // 6 4CallFoo<Bar3>(); // 3 7CallFooFromIA<Bar4>(); // 1CallFooFromIB<Bar4>(); // 2void CallFoo<T>() where T : IC{
    CallFooFromIA<T>();
    CallFooFromIB<T>();
}void CallFooFromIA<T>() where T : IA{
    Console.WriteLine(T.Foo());
}void CallFooFromIB<T>() where T : IB{
    Console.WriteLine(T.Foo());
}interface IA{    virtual static int Foo()
    {        return 1;
    }
}interface IB{    virtual static int Foo()
    {        return 2;
    }
}interface IC : IA, IB{    static int IA.Foo()
    {        return 3;
    }    static int IB.Foo()
    {        return 4;
    }
}struct Bar1 : IC
{    public static int Foo()
    {        return 5;
    }
}struct Bar2 : IC
{    static int IA.Foo()
    {        return 6;
    }
}struct Bar3 : IC
{    static int IB.Foo()
    {        return 7;
    }
}struct Bar4 : IA, IB { }

At the same time, .NET 7 has also improved the numeric types in the base library with abstract and virtual static methods. A large number of generic interfaces for mathematics have been added to System.Numerics, allowing users to use generics to write general mathematical calculation codes:

 using System.Numerics;V Eval<T, U, V>(T a, U b, V c) 
    where T : IAdditionOperators<T, U, U>    where U : IMultiplyOperators<U, V, V>{    return (a + b) * c;
}

Console.WriteLine(Eval(3, 4, 5)); // 35Console.WriteLine(Eval(3.5f, 4.5f, 5.5f)); // 44

▌Generic attribute

C# 11 officially allows users to write and use generic attributes, so we can no longer need to use Type to store type information in attributes, which not only supports type deduction, but also allows users to compile types through generic constraints. limit.

 [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]class FooAttribute<T> : Attribute where T : INumber<T>
{    public T Value { get; }    public FooAttribute(T v)
{
        Value = v;
    }
}

[Foo<int>(3)] // ok[Foo<float>(4.5f)] // ok[Foo<string>("test")] // errorvoid MyFancyMethod() { }

▌ref fields and scoped refs

Starting with C# 11, developers can write ref fields in ref structs, which allow us to store references to other objects in a ref struct:

 int x = 1;
Foo foo = new(ref x);
foo.X = 2;
Console.WriteLine(x); // 2ref struct Foo
{    public ref int X;    
    public Foo(ref int x)
    {
        X = ref x;
    }
}

As you can see, the above code saves the reference to x in Foo, so changes to foo.X will be reflected on x.

If the user does not initialize Foo.X, the default is a null reference, you can use Unsafe.IsNullRef to determine whether a ref is null:

 ref struct Foo
{    public ref int X;    public bool IsNull => Unsafe.IsNullRef(ref X);    
    public Foo(ref int x)
    {
        X = ref x;
    }
}

A problem can be found here, that is, the existence of a ref field may cause the life cycle of an object pointed to by a ref to be extended and cause errors, for example:

 Foo MyFancyMethod(){    int x = 1;
    Foo foo = new(ref x);    return foo; // error}ref struct Foo
{    public Foo(ref int x) { }
}

The above code will report an error when compiling, because foo refers to the local variable x, and the life cycle of the local variable x ends after the function returns, but the operation of returning foo makes the life cycle of foo longer than the life cycle of x, which will lead to problem with invalid references, so the compiler detects this and doesn't allow the code to pass compilation.

But in the above code, although foo does refer to x, the foo object itself does not hold a reference to x for a long time, because it no longer holds a reference to x after the constructor returns, so it should not report an error here. . So C# 11 introduced the concept of scoped, which allows developers to explicitly mark the life cycle of ref. A ref marked with scoped means that the life cycle of this reference will not exceed the life cycle of the current function:

 Foo MyFancyMethod(){    int x = 1;
    Foo foo = new(ref x);    return foo; // ok}ref struct Foo
{    public Foo(scoped ref int x) { }
}

This way, the compiler knows that Foo 's constructor will not cause Foo to still hold a reference to x after the constructor returns, so the above code can safely compile. If we try to escape a scoped ref from the current function, the compiler will complain:

 ref struct Foo
{    public ref int X;    public Foo(scoped ref int x)
    {
        X = ref x; // error
    }
}

In this way, reference safety is achieved.

Using the ref field, we can easily implement various zero-overhead facilities, such as a ColorView that provides multiple ways to access color data:

 using System.Diagnostics.CodeAnalysis;using System.Runtime.CompilerServices;using System.Runtime.InteropServices;var color = new Color { R = 1, G = 2, B = 3, A = 4 };
color.RawOfU32[0] = 114514;
color.RawOfU16[1] = 19198;
color.RawOfU8[2] = 10;
Console.WriteLine(color.A); // 74[StructLayout(LayoutKind.Explicit)]struct Color
{
    [FieldOffset(0)] public byte R;
    [FieldOffset(1)] public byte G;
    [FieldOffset(2)] public byte B;
    [FieldOffset(3)] public byte A;

    [FieldOffset(0)] public uint Rgba;    public ColorView<byte> RawOfU8 => new(ref this);    public ColorView<ushort> RawOfU16 => new(ref this);    public ColorView<uint> RawOfU32 => new(ref this);
}ref struct ColorView<T> where T : unmanaged{    private ref Color color;    public ColorView(ref Color color)
    {        this.color = ref color;
    }

    [DoesNotReturn] private static ref T Throw() => throw new IndexOutOfRangeException();    public ref T this[uint index]
    {
        [MethodImpl(MethodImplOptions.AggressiveInlining)]        get
        {            unsafe
            {                return ref (sizeof(T) * index >= sizeof(Color) ?                    ref Throw() :                    ref Unsafe.Add(ref Unsafe.AsRef<T>(Unsafe.AsPointer(ref color)), (int)index));
            }
        }
    }
}

In fields, ref can also be used with readonly to represent unmodifiable refs, for example:

  • ref int: a reference to an int
  • readonly ref int: a read-only reference to an int
  • ref readonly int: a reference to a read-only int
  • readonly ref readonly int: a readonly reference to a readonly int

This will allow us to secure references so that references to read-only content cannot be accidentally changed.

Of course, ref fields and scoped support in C# 11 are only part of its full shape, and more of it is still being designed and discussed, and will be rolled out in subsequent releases.

▌File local type

C# 11 introduces a new file-local type accessibility notation file that allows us to write types that can only be used in the current file:

 // A.csfile class Foo{    // ...}

file struct Bar
{    // ...}

As a result, if we use these two types in a different file than Foo and Bar, the compiler will complain:

 // A.csvar foo = new Foo(); // okvar bar = new Bar(); // ok// B.csvar foo = new Foo(); // errorvar bar = new Bar(); // error

This feature makes the granularity of accessibility down to the file, which is especially useful for code generators and other code that are to be placed in the same project, but do not want to be touched by others.

▌required members

C# 11 adds required members, members marked with required will be required to be initialized when used, for example:

 var foo = new Foo(); // errorvar foo = new Foo { X = 1 }; // okstruct Foo
{    public required int X;
}

Developers can also use the attribute SetsRequiredMembers to annotate the method, indicating that this method will initialize the required member, so the user does not need to initialize it when using it:

 using System.Diagnostics.CodeAnalysis;var p = new Point(); // errorvar p = new Point { X = 1, Y = 2 }; // okvar p = new Point(1, 2); // okstruct Point
{    public required int X;    public required int Y;

    [SetsRequiredMembers]    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }
}

With required members, we can require that other developers have to initialize some members when using the type we wrote, so that they can use the type we wrote correctly without forgetting to initialize some members.

Computational improvements

▌checked operator

C# has had the concept of checked and unchecked since ancient times, which means checking and unchecking arithmetic overflow respectively:

 byte x = 100;byte y = 200;unchecked{    byte z = (byte)(x + y); // ok}

checked
{    byte z = (byte)(x + y); // error}

In C# 11, the checked operator concept was introduced, allowing users to implement operators for checked and unchecked respectively:

 struct Foo
{    public static Foo operator +(Foo left, Foo right) { ... }    public static Foo operator checked +(Foo left, Foo right) { ... }
}var foo1 = new Foo(...);var foo2 = new Foo(...);var foo3 = unchecked(foo1 + foo2); // 调用 operator +var foo4 = checked(foo1 + foo2); // 调用 operator checked +

For custom operators, the version that implements checked is optional. If the version of checked is not implemented, the unchecked version will be called.

▌Unsigned right shift operator

C# 11 added >>> to represent the unsigned right shift operator. Previously, the right-shift operator >> in C# was a signed right-shift by default, that is, the right-shift operation preserves the sign bit, so for an int, the result will be as follows:

 1 >> 1 = -11 >> 2 = -11 >> 3 = -11 >> 4 = -1// ...

The new >>> is an unsigned right shift operator, which will result in the following:

 1 >>> 1 = 21474836471 >>> 2 = 10737418231 >>> 3 = 5368709111 >>> 4 = 268435455// ...

This saves the trouble of converting the value to an unsigned value and then converting it back when we need an unsigned right shift, and also avoids a lot of unexpected errors caused by it.

▌Shift operators release type restrictions

Beginning with C# 11, the right operand of the shift operator is no longer required to be int, and the type restriction is released like other operators. Therefore, the combination of the above-mentioned abstract and virtual static methods allows us to declare generic shifts. bitwise operators:

 interface ICanShift<T> where T : ICanShift<T>
{    abstract static T operator <<(T left, T right);    abstract static T operator >>(T left, T right);
}

Of course, the above scenario is the main purpose for which this restriction is released. However, I believe that many readers may have an evil idea in their hearts after reading this, yes, cin and cout! While this practice is not recommended in C#, once the restriction is lifted, developers can indeed write code like this:

 using static OutStream;using static InStream;int x = 0;
_ = cin >> To(ref x); // 有 _ = 是因为 C# 不允许运算式不经过赋值而单独成为一条语句_ = cout << "hello" << " " << "world!";public class OutStream{    public static OutStream cout = new();    public static OutStream operator <<(OutStream left, string right)
    {
        Console.WriteLine(right);        return left;
    }
}public class InStream{    public ref struct Ref<T>
    {        public ref T Value;        public Ref(ref T v) => Value = ref v;
    }    public static Ref<T> To<T>(ref T v) => new (ref v);    public static InStream cin = new();    public static InStream operator >>(InStream left, Ref<int> right)
    {        var str = Console.Read(...);
        right.Value = int.Parse(str);
    }
}

▌IntPtr, UIntPtr support numerical operations

In C# 11, both IntPtr and UIntPtr support numerical operations, which greatly facilitates our operations on pointers:

 UIntPtr addr = 0x80000048;
IntPtr offset = 0x00000016;
UIntPtr newAddr = addr + (UIntPtr)offset; // 0x8000005E

Of course, just like the relationship between Int32 and int, Int64 and long, there are also equivalent abbreviations for IntPtr and UIntPtr in C#, which are nint and nuint respectively, and n means native, which is used to indicate the number of digits of this value and the memory of the current operating environment. The address bits are the same:

 nuint addr = 0x80000048;nint offset = 0x00000016;nuint newAddr = addr + (nuint)offset; // 0x8000005E

Pattern matching improvements

▌List pattern matching

List mode was added in C# 11, allowing us to match against lists. In list patterns, we can use [ ] to include our pattern, _ for one element, and .. for 0 or more elements. A variable can be declared after the .. to create a matched sublist containing the elements matched by the .. .

E.g:

 var array = new int[] { 1, 2, 3, 4, 5 };if (array is [1, 2, 3, 4, 5]) Console.WriteLine(1); // 1if (array is [1, 2, 3, ..]) Console.WriteLine(2); // 2if (array is [1, _, 3, _, 5]) Console.WriteLine(3); // 3if (array is [.., _, 5]) Console.WriteLine(4); // 4if (array is [1, 2, 3, .. var remaining])
{
    Console.WriteLine(remaining[0]); // 4
    Console.WriteLine(remaining.Length); // 2}

Of course, like other patterns, the list pattern also supports recursion, so we can use the list pattern in combination with other patterns:

 var array = new int[] { 1, 2, 3, 4, 5 };if (array is [1, 2, 3, 4, 5]) Console.WriteLine(1); // 1if (array is [1, 2, 3, ..]) Console.WriteLine(2); // 2if (array is [1, _, 3, _, 5]) Console.WriteLine(3); // 3if (array is [.., _, 5]) Console.WriteLine(4); // 4if (array is [1, 2, 3, .. var remaining])
{
    Console.WriteLine(remaining[0]); // 4
    Console.WriteLine(remaining.Length); // 2}

▌Pattern matching on Span<char>

In C#, both Span<char> and ReadOnlySpan<char> can be regarded as slices of strings, so C# 11 also adds support for string pattern matching for these two types. E.g:

 int Foo(ReadOnlySpan<char> span){    if (span is "abcdefg") return 1;    return 2;
}

Foo("abcdefg".AsSpan()); // 1Foo("test".AsSpan()); // 2

In this way, the use of Span<char> or ReadOnlySpan<char> scenarios can also be very convenient for string matching, without the need to use SequenceEquals or write loops for processing.

String handling improvements

▌Original string

In C#, @ has been used since the beginning to indicate strings that do not need escaping, but users still need to write "" as "" to include quotes in strings. C# 11 introduced the raw string feature, allowing users to take advantage of raw strings Insert a large amount of text that does not need to be transferred in the code, which is convenient for developers to insert code text in the code in the form of strings.

The original string needs to be surrounded by at least three ", such as """ and """"", etc., and the number of quotation marks before and after should be equal. In addition, the indentation of the original string is determined by the position of the following quotation marks, for example:

 var str = """
    hello
    world
    """;

At this point str is:

 hello
world

And if it is the following:

 var str = """
    hello
    world
""";

str becomes:

 hello
    world

This feature is very useful, for example, we can easily insert JSON code in the code:

 var json = """
    {
        "a": 1,
        "b": {
            "c": "hello",
            "d": "world"
        },
        "c": [1, 2, 3, 4, 5]
    }
    """;
Console.WriteLine(json);/*
{
    "a": 1,
    "b": {
        "c": "hello",
        "d": "world"
    },
    "c": [1, 2, 3, 4, 5]
}
*/

▌UTF-8 string

C# 11 introduced UTF-8 strings, we can use the u8 suffix to create a ReadOnlySpan<byte> that contains a UTF-8 string:

 var str1 = "hello world"u8; // ReadOnlySpan<byte>var str2 = "hello world"u8.ToArray(); // byte[]

UTF-8 is very useful for web scenarios, because in the HTTP protocol, the default encoding is UTF-8, and .NET defaults to UTF-16 encoding, so when dealing with the HTTP protocol, if there is no UTF-8 string, This will result in a large number of conversions between UTF-8 and UTF-16 strings, which will affect performance.

With UTF-8 strings, we can easily create UTF-8 literals to use, no longer need to manually allocate a byte[] and then hardcode the characters we need in it one by one.

▌String interpolation allows newlines

Beginning with C# 11, the interpolation part of the string allows newlines, so the following code becomes possible:

 var str = $"hello, the leader is {group
                                    .GetLeader()
                                    .GetName()}.";

In this way, when the code for interpolation is very long, we can easily format the code without squeezing all the code into one line.

other improvements

▌struct automatic initialization

Starting with C# 11, struct no longer forces the constructor to initialize all fields. For fields that are not initialized, the compiler will automatically zero-initialize them:

 struct Point
{    public int X;    public int Y;    public Point(int x)
{
        X = x;        // Y 自动初始化为 0
    }
}

▌Support nameof for other parameter names

C# 11 allows developers to nameof other parameter names in parameters. For example, when using the attribute CallerArgumentExpression, we previously needed to directly hardcode the string of the corresponding parameter name, but now we only need to use nameof:

 void Assert(bool condition, [CallerArgumentExpression(nameof(condition))] string expression = "")
{    // ...}

This will allow us to automatically modify the content of nameof when modifying the parameter name condition when refactoring the code, which is convenient and reduces errors.

▌Automatically cache delegates for static methods

Starting with C# 11, delegates created from static methods will be automatically cached, for example:

 void Foo(){
    Call(Console.WriteLine);
}void Call(Action action){
    action();
}

Previously, every time Foo was executed, a new delegate would be created from the static method Console.WriteLine, so if Foo was executed in large numbers, a large number of delegates would be created repeatedly, resulting in a large amount of memory being allocated, which is extremely inefficient. Beginning with C# 11, delegates for static methods will be automatically cached, so no matter how many times Foo is executed, Console.WriteLine delegates will only be created once, saving memory and greatly improving performance.

Summarize

Since C# 8, the C# team has been continuously improving the language's type system, greatly improving the language's expressiveness while ensuring static type safety, so that the type system can be a powerful assistant for writing programs, rather than a hindrance.

This update also improves the content related to numerical operations, making it easier for developers to use C# to write numerical calculation methods.

In addition, the exploratory journey of pattern matching is finally coming to an end. After the introduction of list mode, only dictionary mode and active mode are left. Pattern matching is a very powerful tool that allows us to use regular expressions for strings. Easily match data.

In general, there are a lot of new features and improvements in C# 11, each of which has greatly improved the experience of using C#. More exciting new features such as roles and extensions are planned for C# in the future, so let's wait and see.

图片
Long press to identify the QR code and follow Microsoft China MSDN

Click to learn about the new features of C# ~


微软技术栈
418 声望994 粉丝

微软技术生态官方平台。予力众生,成就不凡!微软致力于用技术改变世界,助力企业实现数字化转型。