csharplang/meetings/2018/LDM-2018-06-25.md
Joseph Musser 72a4daa853 Fixed typos (#2167)
* Fixed typos in proposals/

* Fixed typos in meetings/2018/
2019-02-12 13:45:25 -08:00

4.7 KiB

C# Language Design Notes for Jun 25, 2018

Warning: These are raw notes, and still need to be cleaned up. Read at your own peril!

Agenda

  1. Target-typed new-expressions

Target-typed new-expressions

Syntax

C c = new (...){ ... };

You can leave off either the constructor parameters (...) or initializer { ... } but not both, just as when the type is in.

Conversion

This will only work if a) we can determine a unique constructor for C through overload resolution, and b) the object/collection initializer binds appropriately.

But are these errors part of conversion or part of the expression itself? It doesn't matter in a simple example like this, but it matters in overload resolution.

Overload resolution

There are two philosophies we can take on what happens when a target-typed new-expression is passed to an overloaded method.

"late filter" approach

Don't try to weed out overload candidates that won't work with the new-expression, thus possibly causing an ambiguity down the line, or selecting a candidate that won't work. If we make it through, we will do a final check to bind the constructor and object initializer, and if we can't, we'll issue an error.

This reintroduces the notion of "conversion exists with errors" which we just removed in C# 7.3.

"early filter" approach

Consider arguments to constructor, as well as member names in object initializer, as part of applicability of a given overload. Could even consider conversions to members in object initializer. The question is how far to go.

Trade-off

The "early filter" approach is more likely to ultimately succeed - it weeds out things that will fail later before they get picked. It does mean that it relies more on the specifics of the chosen target type for overload resolution, so it is more vulnerable to changes to those specifics.

struct S1 { public int x; }
struct S2 {}

M(S1 s1);
M(S2 s2);

M(new () { x = 43 }); // ambiguous with late filter, resolved with early. What does the IDE show?

Adding constructors to the candidate types can break both models. Adding fields, properties, members called Add, implementing IEnumerable can all potentially break in the early filter model.

M2(Func<S1> f);
M2(Func<S2> f);
M2(() => new () { x = 43 });

S1 Foo() => new () { x = 43 };

Even if we did late filtering, this would probably work (i.e. the S2 overload would fail), because "conversion with error" would give an error in the lambda, which in itself rules out the overload.

We're having a hard time thinking of practical scenarios where the difference really matters. Only if we go to the "extremely early" position where the expression could contribute even to type inference. We've previously considered:

M<T>(C<T> c);
M(new C (...) { ... });

Where the type arguments to C could be left off and inferred from the new expression. This would take it a bit further and allow

M (new (...) {...});

In that same setup, contributing to type inference from the innards of an implicit new expression.

Conclusion

We are good with late checking for now. This does mean that we reintroduce the notion of conversion with errors.

Breaking change

As mentioned this introduces a new kind of breaking change in source code, where adding a constructor can influence overload resolution where a target-typed new expression is used in the call.

Unconstructible types

That said, we could define a set of types which can never be target types for new expressions. That is not subject to the same worries as the discussion above, where the innards of the new expression could potentially affect overload resolution. These are overloads where no implicit new expression could ever work.

Candidates for unconstructible types:

  • Pointer types
  • array types
  • abstract classes
  • interfaces
  • enums

Tuples are constructible. You can use ValueTuple overloads. Delegates are constructible.

Nullable value types

Without special treatment, they would only allow the constructors of nullable itself. Not very useful. Should they instead drive constructors of the underlying type?

S? s = new (){}

Conclusion

Yes

Natural type

Target-typed new doesn't have a natural type. In the IDE experience we will drive completion and errors from the target type, offering constructor overloads and members (for object initializers) based on that.

Newde

Should we allow stand-alone new without any type, constructor arguments or initializers?

No. We don't allow new C either.

Dynamic

We don't allow new dynamic(), so we shouldn't allow new() with dynamic as a target type.

For constructor parameters that are dynamic there is no new/special problem.