129 lines
No EOL
4.7 KiB
Markdown
129 lines
No EOL
4.7 KiB
Markdown
# 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 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.
|
|
|
|
``` c#
|
|
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.
|
|
|
|
``` c#
|
|
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:
|
|
|
|
``` c#
|
|
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
|
|
|
|
``` c#
|
|
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?
|
|
|
|
``` c#
|
|
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. |