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
- 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.
Unconstructable 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 unconstructable types:
- Pointer types
- array types
- abstract classes
- interfaces
- enums
Tuples are constructable. You can use ValueTuple
overloads.
Delegates are constructable.
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.