Skip to content

Latest commit

 

History

History
115 lines (67 loc) · 5.74 KB

LDM-2018-01-24.md

File metadata and controls

115 lines (67 loc) · 5.74 KB

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.

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:

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.

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

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

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

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.