csharplang/meetings/2014/LDM-2014-02-03.md
2018-01-25 13:11:49 -08:00

6.9 KiB
Raw Blame History

C# Language Design Notes for Feb 3, 2014

Agenda

We iterated on some of the features currently under implementation

  1. Capture of primary constructor parameters <only when explicitly asked for with new syntax>
  2. Grammar around indexed names <details settled>
  3. Null-propagating operator details <allow indexing, bail with unconstrained generics>

Capture of primary constructor parameters

Primary constructors as currently designed and implemented lead to automatic capture of parameters into private, compiler-generated fields of the object whenever those parameters are used after initialization time.

It is becoming increasingly clear that this is quite a dangerous design. To illustrate, whats wrong with this code?

public class Point(int x, int y)
{
    public int X { get; set; } = x;
    public int Y { get; set; } = y;
    public double Dist => Math.Sqrt(x * x + y * y);
    public void Move(int dx, int dy)
    {
        x += dx; y += dy;
    }
}

This appears quite benign, but is in fact catastrophically wrong. The use of x and y in Dist and Move causes these values to be captured as private fields. The auto-properties X and Y each cause their own backing fields to be generated, initialized with the x and y values passed in to the primary constructors. But from then on, X and Y lead completely distinct lives from x and y. Assignments to the X and Y properties will cause them to be observably updated, but the value of Dist remains unchanged. Conversely, changes through the Move method will reflect in the value of Dist, but not affect the value of the properties.

The way for the developer to avoid this is to be extremely disciplined about not referencing x and y except in initialization code. But that is like giving them a gun already pointing at their foot: sooner or later it will go subtly wrong, and they will have hard to find bugs.

There are other incarnations of this problem, e.g. where the parameter is passed to the base class and captured multiple times.

There are also other problems with implicit capture: we find, especially from MVP feedback, that people quickly want to specify certain things about the generated fields, such as readonly-ness, attributes, etc. We could allow those on the parameters, but they quickly dont look like parameters anymore.

The best way for us to deal with this is to simply disallow automatic capture. The above code would be disallowed, and given the same declarations of x, y, X and Y, Dist and Move would have to written in terms of the properties:

    public double Dist => Math.Sqrt(X * X + Y * Y);
    public void Move(int dx, int dy)
    {
        X += dx; Y += dy;
    }

Now this raises a new problem. What if you want to capture a constructor parameter in a private field and have no intention of exposing it publicly. You can do that explicitly:

public class Person(string first, string last)
{
    private string _first = first;
    private string _last = last;
    public string Name => _first + " " + _last;
}

The problem is that the “good” lower case names in the class-level declaration space are already taken by the parameters, and the privates are left with (what many would consider) less attractive naming options.

We could address this in two ways (that we can think of) in the primary constructor feature:

  1. Allow primary constructor parameters and class members to have the same names, with the excuse that their lifetimes are distinct: the former are only around during initialization, where access to the latter through this is not yet allowed.
  2. Introduce a syntax for explicitly capturing a parameter. If you ask for it, presumably you thought through the consequences.

The former option seems mysterious: two potentially quite different entities get to timeshare on the same name? And then youd get confusing initialization code like this:

    private string first = first; // WHAT???
    private string last = last;

It seems that the latter option is the better one. We would allow field-like syntax to occur in a parameter list, which is a little odd, but kind of says what it means. Specifically specifying an accessibility on a parameter (typically private) would be what triggers capture as a field:

public class Person(private string first, private string last)
{
    public string Name => _first + " " + _last;
}

Once theres an accessibility specified, we would also allow other field modifiers on the parameter; readonly probably being the most common. Attributes could be applied to the field in the same manner as with auto-properties: through a field target.

Conclusion We like option two. Lets add syntax for capture and not do it implicitly.

Grammar for indexed names For the lightweight dynamic features, weve been working with a concept of “pseudo-member” or indexed name for the $identifier notation.

We will introduce this as a non-terminal in the grammar, so that the concept is reified. However, for the constructs that use it (as well as ordinary identifiers) we will create separate productions, rather than unify indexed names and identifiers under a common grammatical category.

For the stand-alone dictionary initializer notation of [expression] we will not introduce a non-terminal.

Null-propagating operator details

Nailing down the design of the null-propagating operator we need to decide a few things:

Which operators does it combine with?

The main usage of course is with dot, as in x?.y and x?.m(…). It also potentially makes sense for element access x?[…] and invocation x?(…). And we also have to consider interaction with indexed names, as in x?.$y.

Well do element access and indexed member access, but not invocation. The former two make sense in the context that lightweight dynamic is addressing. Invocation seems borderline ambiguous from a syntactic standpoint, and for delegates you can always get to it by explicitly calling Invoke, as in d?.Invoke(…).

Semantics

The semantics are like applying the ternary operator to a null equality check, a null literal and a non-question-marked application of the operator, except that the expression is evaluated only once:

e?.m()   =>   ((e == null) ? null : e0.m())
e?.x      =>   ((e == null) ? null : e0.x)
e?.$x     =>   ((e == null) ? null : e0.$x)
e?[]     =>   ((e == null) ? null : e0[])

Where e0 is the same as e, except if e is of a nullable value type, in which case e0 is e.Value.

Type

The type of the result depends on the type T of the right hand side of the underlying operator:

  • If T is (known to be) a reference type, the type of the expression is T
  • If T is (known to be) a non-nullable value type, the type of the expression is T?
  • If T is (known to be) a nullable value type, the type of the expression is T
  • Otherwise (i.e. if it is not known whether T is a reference or value type) the expression is a compile time error.