- Ref reassignment
- New constraints
- Target typed stackalloc initializers
- Deconstruct as ref extension method
Proposal here.
C# 7.0 added ref locals, but did not allow them to be reassigned with other refs. In C# 7.3 we are looking to allow that, and have a handle on the rules that need to be in place to make it safe.
The proposal adds a new ref-assignment expression of the form r = ref v
. This makes r
point to the storage location that v
points to. The result of the expression is the variable designating that storage location. The variable can e.g. be evaluated or assigned to:
while ((l = ref l.Next) != null) ... // linked list walk
The linked list walk example shows why it is beneficial to have it be an expression form just like regular assignment, rather than just a statement form. Of course, just like with regular assignment, using it as an expression is easily overused; we think there is enough cultural awareness around this.
Into the future we need to maintain a principle that expressions never start with ref
. Therefore, ref
in front of an expression is always part of an enclosing construct. This principle will help people (a little) when reasoning about these expressions.
The compiler tracks lifetimes of variables, in order to make sure that a variable is not referenced by something that will outlast it. In C# 7.0 a ref local is simply created with the lifetime of the variable that it is initialized with.
In a ref assignment expression, the compiler maintains lifetime safety by requiring that the lifetime of the right-hand side is at least as long as the lifetime of the left-hand side.
With ref reassignment it now becomes meaningful to allow ref (and ref readonly) locals to be left uninitialized at declaration. In that case what should be their lifetime? We can discuss that later.
Iteration variables declared in foreach
and for
loops can now be ref
. In the case of for
loops this only makes sense because ref reassignment is allowed.
Foreach iteration variables can never be reassigned in the body, and that is also the case for ref iteration variables: they cannot be ref reassigned.
Let's finalize the new forms of constraints.
void M<T1, T2, T3>()
where T1: unmanaged
where T2: Enum
where T3: Delegate
{
}
It should be called unmanaged
, not some other word like blittable
. F#, the runtime and the C# spec all use the term "unmanaged" for types that you can take a pointer to.
It can't be a keyword (that would be breaking) and not even a contextual keyword (in the sense that there's a syntactic context that determines it). Instead it needs to be semantically recognized, like var
.
Just like var
, it can be escaped to make clear that you are using it as an identifier. If a type of that name is in scope, there's no way to get at the constraint meaning of it. So just like var
, a devious person can declare an unmanaged
type to prevent people from using the unmanaged constraint.
It is useful to allow System.Delegate
as a constraint, as it has several methods on it. It doesn't guarantee that the type argument would be a delegate type; it could be Delegate
itself, or MulticastDelegate
, etc.
Let's just unblock Delegate
and MulticastDelegate
from being a constraint, and give it no special meaning. This is useful and reduces complexity in the language.
Same for enum. We could imagine a special enum
constraint that means Enum + struct
, but instead let's just stop disallowing System.Enum
. People should be allowed to manually write
where T : struct, Enum
We a special rule to allow struct
and Enum
together, since otherwise only interfaces are allowed to combine with the struct
constraint.
There are a few other non-sealed reference types that are prevented from being used as constraints, e.g. Array
and ValueType
. While it is tempting to unblock them all, now that we're at it, let's not rock that boat until we have useful scenarios for it.
var x = new int[] { 1, 2, 3 }; // allowed today
var z = stackalloc int[5]; // z is int* for back compat
Span<int> zs = stackalloc int[5]; // target typed
var y = stackalloc int[] { 1, 2, 3 }; // should be allowed? y is int*
Span<int> ys = stackalloc int[] { 1, 2, 3 }; // should be allowed? y is Span<int>
The int*
is for back compat. In contexts other than this the natural type of stackalloc
is Span<T>
. That's a bit inconsistent, and there are probably other ways to skin the cat, but they would probably add more syntax and not reduce the inconsistency - just push it around. So we're good with this.
Should we allow the element type to be inferred from the initializer, just as we do with array initializers? Yes.
public static void Deconstruct(this int i, out int x, out int y); // 1
public static void Deconstruct(this in int i, out int x, out int y); // 2
public static void Deconstruct(this ref int i, out int x, out int y); // 3
Currently 1 and 2 are eligible as deconstructors, whereas 3 is not. you could argue that it is an arbitrary and strangely specific limitation. On the other hand, 3 is probably never desirable, as a deconstructor should not wish to mutate its receiver!
Fixing this does not seem to add any value. Let's keep it the way it is.