Preface
A heavyweight feature is coming in C# 11 that can ecstasy for developers who value performance. This feature mainly revolves around an important underlying performance facilityref
andstruct
a series of improvements.
However, this part of the improvement involves a lot of content and may not be completed in .NET 7 (C# 11), so it is also possible to postpone some of the content to C# 12. Of course, it is still very hopeful that you can see the completeness at the time of C# 11.
This article only introduces this feature, because in addition to this feature, there are many other improvements in C# 11. There are so many articles that can’t be finished. Let’s wait until .NET 7 is officially released.
background
C# has introduced new versions since 7.0ref struct
Used to represent objects on the stack that cannot be boxed, but at that time it was very limited and could not even be used for generic constraints or asstruct
field. In C# 11, due to the characteristicsref
The field push requires allowing types to hold references to other value types, and things in this area have finally made great progress.
These facilities are designed to allow developers to write high-performance code using secure code without facing unsafe pointers. Next I will introduce the upcoming improvements in C# 11 or even 12 in this regard.
ref field
C# used to be able to hold references to other value types in types, but in C# 11, this will become possible. Starting with C# 11, it will be allowedref struct
definitionref
Field.
readonly ref struct Span<T> { private readonly ref T _field; private readonly int _length; public Span(ref T value) { _field = ref value; _length = 1; } }
Intuitively, such a feature will allow us to write the above code, which constructs aSpan<T>
, it holds for othersT
Reference to the object.
certainly,ref struct
Can bedefault
To initialize:
Span<int> span = default;
But this_field
It will be an empty reference, but we can passMethod to check:
if ((ref _field)) { throw new NullReferenceException(...); }
in addition,ref
The modifiability of fields is also a very important thing, so:
-
readonly ref
: A read-only reference to an object, which cannot be constructed by itself orinit
Modified outside the method -
ref readonly
: A reference to a read-only object that cannot be modified outside the constructor or init method -
readonly ref readonly
: A read-only reference to a read-only object is a combination of the above two
For example:
ref struct Foo { ref readonly int f1; readonly ref int f2; readonly ref readonly int f3; void Bar(int[] array) { f1 = ref array[0]; // no problem f1 = array[0]; // Error, because the value referenced by f1 cannot be modified f2 = ref array[0]; // Error, because f2 itself cannot be modified f2 = array[0]; // no problem f3 = ref array[0]; // Error: Because f3 itself cannot be modified f3 = array[0]; // Error: Because the value referenced by f3 cannot be modified } }
life cycle
All of this looks beautiful, but is there really no problem?
Suppose we have the following code to use the above:
Span<int> Foo() { int v = 42; return new Span<int>(ref v); }
v
It is a local variable, and its life cycle will end after the function returns, so the above code will causeSpan<int>
Holdv
The reference becomes invalid. By the way, the above code is completely legal because C# did not support it beforeref
field, so the above code is impossible to have escape problems. But C# 11 joinedref
The fields, objects on the stack may passref
Reference escape occurs in fields, so the code becomes insecure.
If we have oneCreateSpan
Methods are used to create a referenceSpan
:
Span<int> CreateSpan(ref int v) { // ... }
This leads to a series of problems in the previous C# (becauseref
The life cycle is the current method), but in C# 11 due to possible existenceref
Fields lead to non-secure code written in a safe way:
Span<int> Foo(int v) { // 1 return CreateSpan(ref v); // 2 int local = 42; return CreateSpan(ref local); // 3 Span<int> span = stackalloc int[42]; return CreateSpan(ref span[0]); }
Therefore, in C# 11, destructive changes have to be introduced, and the above code is not allowed to be compiled. But that doesn't completely solve the problem.
To solve the escape problem, C# 11 formulates rules for citing escape safety. For ae
Fields inf
:
- if
f
It's aref
field, ande
yesthis
,butf
In the method it is surrounded, it is referenced to escape security - Otherwise if
f
It's aref
field, thenf
Reference escape security scope ande
The escape safety range is the same - Otherwise if
e
is a reference type,f
The reference escape safe range is the method that calls it - otherwise
f
Reference escape security scope ande
same - Since methods in C# can return references, according to the above rules,
ref struct
The method in the process will not return a non-ref
References to fields:
ref struct Foo { private ref int _f1; private int f2; public ref int P1 => ref _f1; // no problem public ref int P2 => ref _f2; // Error, because of a violation of rule 4}
In addition to citing escape security rules, there is alsoref
Rules for assignment:
- for
x.e1 = ref e2
, inx
It is safe to escape in calling methods, thene2
Must be referenced in the call method to escape safely - for
e1 = ref e2
,ine1
It's a local variable, thene2
The reference escape security range must be at least ande1
The quotation escape security range is as large as
So, according to the above rules, the following code is fine:
readonly ref struct Span<T> { readonly ref T _field; readonly int _length; public Span(ref T value) { // No problem, because x is this, the escape security range of this and the reference escape security range of value are both called methods, satisfying rule 1 _field = ref value; _length = 1; } }
So it is natural that the life cycle needs to be marked on the fields and parameters to help the compiler determine the escape range of the object.
When we write code, we don’t need to remember so many of the above rules, because everything becomes explicit and intuitive after life cycle annotation.
scoped
In C# 11,scoped
Keywords are used to limit the security range of escape:
Local variable s | Quote Escape Security Range | Escape security range |
---|---|---|
Span<int> s |
Current method | Calling methods |
scoped Span<int> s |
Current method | Current method |
ref Span<int> s |
Calling methods | Calling methods |
scoped ref Span<int> s |
Current method | Calling methods |
ref scoped Span<int> s |
Current method | Current method |
scoped ref scoped Span<int> s |
Current method | Current method |
in,scoped ref scoped
It is redundant because it can beref scoped
Implicit. And we just need to knowscoped
It is used to limit the escape range to the current method. Isn't it very simple?
In this way, we can mark the escape range (life cycle) of the parameters:
Span<int> CreateSpan(scoped ref int v) { // ... }
Then, the previous code becomes fine because it is allscoped ref
:
Span<int> Foo(int v) { // 1 return CreateSpan(ref v); // 2 int local = 42; return CreateSpan(ref local); // 3 Span<int> span = stackalloc int[42]; return CreateSpan(ref span[0]); }
scoped
It can also be used on local variables:
Span<int> Foo() { // Error, because span cannot escape the current method scoped Span<int> span1 = default; return span1; // No problem, because the initializer's escape safe range is the calling method, because span2 can escape to the calling method Span<int> span2 = default; return span2; // span3 and span4 are the same, because the escape safe range of the initializer is the current method, and there is no difference between adding scoped or not Span<int> span3 = stackalloc int[42]; scoped Span<int> span4 = stackalloc int[42]; }
in addition,struct
ofthis
Added tooscoped ref
The escape range of , that is, the escape safe range is referenced as the current method, and the escape safe range is the calling method.
The rest is andout
、in
The combination of parameters, in C# 11,out
The parameters will default toscoped ref
,andin
The parameters remain defaultref
:
ref int Foo(out int r) { r = 42; return ref r; // Error, because the reference escape safe range of r is the current method}
This is very useful, for example, in the following common situation:
Span<byte> Read(Span<byte> buffer, out int read) { // .. } Span<int> Use() var buffer = new byte[256]; // If the reference escape security range of out is not modified, this will report an error because the compiler needs to consider that read can be returned as a ref field // If you modify the reference escape security scope of out, there is no problem, because the compiler does not need to consider that read can be returned as a ref field int read; return Read(buffer, out read);
Here are some more examples:
Span<int> CreateWithoutCapture(scoped ref int value) { // Error, because the reference escape safe range of value is the current method return new Span<int>(ref value); } Span<int> CreateAndCapture(ref int value) // No problem, because the escape security range of value is limited to the reference escape security range of value, which is the calling method return new Span<int>(ref value) Span<int> ComplexScopedRefExample(scoped ref Span<int> span) // No problem, because the escape safe range of span is to call the method return span; // No problem, because the reference escape security range of refLocal is the current method, and the escape security range is the calling method // In the call to ComplexScopedRefExample it is passed to a scoped ref parameter, // means that the compiler does not need to consider referenced escape security scope when calculating the life cycle, but only needs to consider escape security scope. // Therefore, the safe escape range of the value it returns is to call the method Span<int> local = default; ref Span<int> refLocal = ref local; return ComplexScopedRefExample(ref refLocal); // Error, because stackLocal's reference escape security range and escape security range are both current methods // Therefore the safe escape range of the value it returns is the current method Span<int> stackLocal = stackalloc int[42]; return ComplexScopedRefExample(ref stackLocal);
unscoped
In the above design, there is still a problem that has not been solved:
struct S { int _field; // Error, because this reference escape safe range is the current method public ref int Prop => ref _field; }
Therefore, introduce aunscoped
, allowing the escape range to be extended to the calling method, so the above method can be rewritten as:
struct S { private int _field; // No problem, the reference escape security scope has been extended to the calling method public unscoped ref int Prop => ref _field; }
thisunscoped
Can also be placed directlystruct
superior:
unscoped struct S { private int _field; public unscoped ref int Prop => ref _field; }
Similarly, nestedstruct
No problem either:
unscoped struct Child { int _value; public ref int Value => ref _value; } unscoped struct Container { Child _child; public ref int Value => ref _child.Value; }
Also, if needed to restore the previous oneout
If you escape, you can alsoout
Specified on the parametersunscoped
:
ref int Foo(unscoped out int r) { r = 42; return ref r; }
But it's relatedunscoped
The design is still in the preliminary stage and will not be provided in C# 11.
ref struct constraint
Starting with C# 11,ref struct
It can be used as a generic constraint, so you can write the following method:
void Foo<T>(T v) where T : ref struct { // ... }
therefore,Span<T>
The functionality of theSpan<Span<T>>
For example, use it inbyte
orchar
It can be used for high-performance string processing.
reflection
With so many things above, reflection naturally needs to be supported. Therefore, the reflection API is also addedref struct
Related support.
Practical use cases
With the above infrastructure, we can use security code to build some high-performance wheels.
Fixed length list on stack
struct FrugalList<T> { private T _item0; private T _item1; private T _item2; public readonly int Count = 3; public unscoped ref T this[int index] => index switch { 0 => ref _item1, 1 => ref _item2, 2 => ref _item3, _ => throw new OutOfRangeException("Out of range.") }; }
Stack-on table
ref struct StackLinkedListNode<T> { private T _value; private ref StackLinkedListNode<T> _next; public T Value => _value; public bool HasNext => !(ref _next); public ref StackLinkedListNode<T> Next => HasNext ? ref _next : throw new InvalidOperationException("No next node."); public StackLinkedListNode(T value) { this = default; _value = value; } public StackLinkedListNode(T value, ref StackLinkedListNode<T> next) _next = ref next; }
In addition to these two examples, others are like parsers and serializers, etc.Utf8JsonReader
、Utf8JsonWriter
These things can be used.
Future plans
Advanced Life Cycle
Although the above life cycle design can meet most uses, it is still not flexible enough. Therefore, it is possible to expand on this basis in the future and introduce advanced life cycle annotations. For example:
void M(scoped<'a> ref MyStruct s, scoped<'b> Span<int> span) where 'b >= 'a { = span; }
The above method gives parameterss
andspan
Two life cycles were declared separately'a
and'b
and constrain'b
The life cycle is no less than'a
, so in this method,span
Can be assigned safely to。
Although this will not be included in C# 11, if developers' demand for related things increases in the future, it may be subsequently added to C#.
Summarize
The above is C# 11 (or afterwards)ref
andstruct
Improved. With these infrastructures, developers will be able to easily write high-performance code without any heap memory overhead in a secure way. Although these improvements can only directly impose a small number of developers who pay great attention to performance, these improvements will bring about overall improvements in the quality and performance of the subsequent basic library code.
If you are worried that this will increase the complexity of the language, it is not necessary, because most people will not use these things, and will only affect a small number of developers. Therefore, for most people, they just need to write the same code and enjoy other basic library authors writing what they use the above facilities.
This is the end of this article about C# 11’s improvements to ref and struct. For more related C# 11’s improvements to ref and struct, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!