115 lines
5.7 KiB
Markdown
115 lines
5.7 KiB
Markdown
# C# Language Design Notes for Jan 24, 2018
|
|
|
|
## Agenda
|
|
|
|
1. Ref reassignment
|
|
2. New constraints
|
|
3. Target typed stackalloc initializers
|
|
4. Deconstruct as ref extension method
|
|
|
|
# Ref reassignment
|
|
|
|
Proposal [here](https://github.com/agocke/roslyn/blob/4de95445af874269e74f9b022d83c89d85ec9669/docs/features/ref-reassignment.md).
|
|
|
|
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.
|
|
|
|
## Ref assignment expressions
|
|
|
|
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:
|
|
|
|
``` c#
|
|
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.
|
|
|
|
## Lifetimes
|
|
|
|
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.
|
|
|
|
## Uninitialized ref locals
|
|
|
|
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.
|
|
|
|
## Ref locals in looping constructs
|
|
|
|
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.
|
|
|
|
|
|
# New constraints
|
|
|
|
Let's finalize the new forms of constraints.
|
|
|
|
``` c#
|
|
void M<T1, T2, T3>()
|
|
where T1: unmanaged
|
|
where T2: Enum
|
|
where T3: Delegate
|
|
{
|
|
|
|
}
|
|
```
|
|
|
|
## Unmanaged
|
|
|
|
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.
|
|
|
|
|
|
## Delegate
|
|
|
|
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.
|
|
|
|
## Enum
|
|
|
|
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
|
|
|
|
``` c#
|
|
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.
|
|
|
|
## Other non-sealed reference types?
|
|
|
|
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.
|
|
|
|
|
|
# Target typed stackalloc initializers
|
|
|
|
``` c#
|
|
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.
|
|
|
|
|
|
# Deconstruct as ref extension method
|
|
|
|
``` c#
|
|
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.
|
|
|