csharplang/meetings/2018/LDM-2018-01-03.md
2021-07-27 12:34:38 -07:00

5.7 KiB

C# Language Design Notes for Jan 3, 2018

Quote of the Day: "What about CallerPoliticalAffiliationAttribute?"

Agenda

  1. Scoping of expression variables in constructor initializer
  2. Scoping of expression variables in field initializer
  3. Scoping of expression variables in query clauses
  4. Caller argument expression attribute
  5. Other caller attributes
  6. New constraints

Scoping of expression variables in constructor initializer

Should an expression variable x introduced in a this or base initializer be available in the constructor body?

C(int i) : base(out var x) { ... x ... }

It feels natural and occasionally useful that x would be in scope in the body.

Any reason not to allow it? Not at the language level. There's a design question as to how it's represented in IOperation, but that's not a language question.

Conclusion

Let x be in scope in the whole constructor, including the body.

Scoping of expression variables in field initializer

Where should an expression variable x introduced in a field initializer be in scope?

class C
{
    int i = M(out int x)
            .N(x), // here?
        y = M(x);  // here?
    int j = x;     // here?
    int X => x;    // here?
}

A number of options:

  1. Only in the same initializer expression
  2. In the whole field declaration, including initializers for subsequent fields
  3. In all subsequent initializers in the class declaration
  4. In the whole class body, including function members (so it would effectively become a private field if necessary)

There's something tempting about 4, especially considering the relatively loose scope rules we chose for expression variables inside statement lists.

void M()
{
    int i = N(out int x);
    int M() => x; // in scope here
}

class C
{
    int i = N(out int x);
    int M() => x; // why not here?
}

Also, if we ever do primary constructors, and want to allow statements interspersed with member declarations, we'll probably wish we did 4.

However, there are also difficult questions with 4: can it be used before it is declared? Is it visible across partial classes? These are all questions that could be answered, but we are also a little uneasy making it so easy to "accidentally" declare an extra private field, in case some function members close over it.

That latter concern could be alleviated by reducing to option 3, but now the scope is weird, and most of the usefulness is gone. May as well reduce to 2 or 1 then.

Conclusion

We'll go with option 1. If you want to use expression variables to carry information between initializers, you'll instead have to do your initialization in the constructor body.

The script dialect will have to stay consistent, so declaration variables must be lifted to fields, and be visible in subsequent statements.

Scoping of expression variables in query clauses

What should be the scope of an expression variable x that occurs in a query clause?

It seems desirable at first that the variables would carry over from clause to clause:

from x in e
where M(x, out var y)
select y

However, to do this we would need to perform much more semantic analysis of the query than we do today, before we lower. It feels out of touch with the syntactic flavor of queries today.

Instead, a more reasonable approach may be to make it easier to carry multiple range variables forward from a let (and maybe from) clause, using deconstruction syntax:

from x in e
let (m, z) = (M(x, out var y), y)
select z;

Conclusion

We should allow expression variables in queries, but keep them scoped to the individual query clauses. In other words, they aren't range variables. They are normal variables scoped by the lambdas that we translate into.

Caller argument expression attribute

csharplang/issues/287

The proposal calls for an extra parameter with a default value (which is then replaced by the expression passed as an argument for the parameter designated in the attribute). This means that in a tie breaker situation, existing methods would match better than new ones that differ only by having this extra argument.

There are solutions to that for API owners:

  1. Take a binary breaking change (that's probably what XUnit would do)
  2. Use different API names (won't be picked up automatically by old code)
  3. Change in ref assemblies only (might for Debug.Assert)

In summary, folks have a decent slate of options.

Conclusion

Fine to accept this one.

Other caller attributes

We are not comfortable with those just yet. We'd need to work on the details. For now we are suspicious of having a record of a lot of different information, and of the number of proposed ones in csharplang/issues/87.

Constraints

csharplang/issues/103 and csharplang/issues/104 propose to allow the Enum and Delegate types as constraints. No special meaning, just stop disallowing them. That's a starting point. Adding keywords enum and delegate could be discussed later.

csharplang/issues/187 proposes a contextual keyword unmanaged; we would represent it by putting a ModReq in the constraint of the parameter. It implies the struct constraint.

Reference assemblies are a problem: some tools produce reference assemblies that remove private fields from structs, which already leads to semantic problems in the compiler (definite assignment, allowing pointers to them), and now would lead to more. That's fundamentally not a language problem though.