diff --git a/proposals/inference-for-constructor-calls.md b/proposals/inference-for-constructor-calls.md index f8a7321500..c17fd00d86 100644 --- a/proposals/inference-for-constructor-calls.md +++ b/proposals/inference-for-constructor-calls.md @@ -1,83 +1,96 @@ -# Target-typed inference for constructor calls +# Type inference for constructor calls -*This proposal builds on the [target-typed generic type inference](https://github.com/dotnet/csharplang/blob/main/proposals/target-typed-generic-type-inference.md) proposal. +Champion issue: [dotnet/csharplang#9627](https://github.com/dotnet/csharplang/issues/9627) ## Summary +[summary]: #summary -Generic type inference is extended to 'new' expressions, which may infer type arguments for the newly created class or struct, including [from a target type](target-typed-generic-type-inference) if present. For instance, given: +An object creation can omit the type arguments of a generic type when they can be inferred from the constructor arguments and target type. + +## Motivation +[motivation]: #motivation + +Generic types often have factory methods only because method calls can infer type arguments and constructor calls cannot: ```csharp -public class MyCollection : IEnumerable -{ - public MyCollection() { ... } - ... -} +var pair = Pair.Create("answer", 42); +var pair = new Pair("answer", 42); // Pair ``` -We would allow the constructor to be called without a type argument when it can be inferred from arguments or (in this case) a target type: +This is also needed when a generic case type is constructed through its generic base type: ```csharp -IEnumerable c = new MyCollection(); // 'T' = 'string' inferred from target type +Option option = new Some("Hello"); // Some ``` -## Motivation +Today `Some` must be written explicitly or hidden behind a factory method even though the argument and target contain the required type information. -Even without [target-typed generic type inference](https://github.com/dotnet/csharplang/blob/main/proposals/target-typed-generic-type-inference.md) constructors are at a disadvantage to factory methods, because type arguments cannot be inferred on invocation. This frequently leads to trivial factory methods being declared simply for the benefit of type inference. If target-typed inference is added, this convenience gap is only going to grow. +The [type groups proposal](https://github.com/dotnet/csharplang/pull/10310) supplies the same-name types considered by object creation. The [generalized inference proposal](https://github.com/dotnet/csharplang/pull/10311) supplies the shared inference mechanism. The existing [target-typed generic type inference](target-typed-generic-type-inference.md) proposal owns target-derived behavior. -For constructor calls we already have "target type new" for when a target type is exactly the type to be created, but even when that's not the case, arguments or the target type often have sufficient information between them that the type arguments for the constructed type could be inferred. +## Detailed design +[design]: #detailed-design -This is frequently the case for [closed hierarchies](https://github.com/dotnet/csharplang/blob/main/proposals/csharp-15.0/closed-hierarchies.md) and [unions](https://github.com/dotnet/csharplang/blob/main/proposals/nominal-type-unions.md). Most commonly, a target type will be the closed class or union type itself, whereas the constructed type will be one of the case types: +The authoritative specification changes are in [csharpstandard#3](https://github.com/MadsTorgersen/csharpstandard/pull/3). They update [§12.6.3 Type inference](https://github.com/dotnet/csharpstandard/blob/alpha-v12/standard/expressions.md#1263-type-inference) and [§12.8.17.2 Object creation expressions](https://github.com/dotnet/csharpstandard/blob/alpha-v12/standard/expressions.md#128172-object-creation-expressions). -```csharp -Option option = new Some("Hello"); -``` +### Syntax and lookup -With this proposal, it could be written simply as +Object creation accepts the `type_group` defined by the type groups proposal. Lookup can therefore produce accessible same-name types of different arities. An explicitly bound type, including the implied type of `new()`, produces a singleton group. -```csharp -Option option = new Some("Hello"); // Infer 'string' from argument and target type -``` +Lookup completes before constructors are considered. Constructor binding operates on the resulting group. -## Detailed specification +### Constructor inference and candidates -The proposal is specified by treating the constructor "as if" it were a generic method, and the constructor call "as if" it were an invocation of that generic method. +For each accessible constructor of each unbound generic type in the group, inference uses: -In an *object_creation_expression* of the form `new T(E₁ ...Eₓ)` where `T` has no type argument list, if there is an accessible non-generic type `T` and it has one or more accessible and applicable constructors, then overload resolution proceeds as today. +- the containing type's type parameters; +- the constructor parameter types and corresponding object-creation arguments; +- the containing type with its type parameters as the result type; and +- the target type of the object creation, if any. -However, if no such type or constructors are found, then for each generic type `T` with the same type name `T`, and for each constructor of that generic type with the parameter list `(T₁ p₁ ... Tₓ pₓ)`, [generic type inference](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#1263-type-inference) is attempted, as if the constructor were a generic method `M` with the same type parameter list as `T`, with the same parameter list `(T₁ p₁ ... Tₓ pₓ)` as the constructor, and with T as its return type: +This supports inference from the target alone or together with arguments: ```csharp -T M(T₁ p₁ ... Tₓ pₓ) +Box box = new Box(); // Box +IEnumerable list = new C(5); // C ``` -and as if the object creation expression were an invocation with no type arguments, and with the same argument list and target type `I` (if any) as the creation expression: +In the second example, the argument infers `C`'s count type and the target infers its element type. The linked inference proposals define how these inputs produce bounds and type arguments. -```csharp -M(E₁ ...Eₓ) // without target type -I i = M(E₁ ...Eₓ) // with target type -``` +Inference is performed separately for each constructor. Failure to match arguments to parameters or infer all containing-type arguments removes that constructor from consideration. -(Where the names `i` and `M` are otherwise invisible and not in conflict with any other names in scope.) +After substitution, the constructed containing type and constructed types in the parameter list must satisfy their constraints, and the substituted parameter list must be applicable to the arguments. Constructors of bound types require no inference and are candidates when accessible and applicable. A struct's implicit default constructor participates on the same basis. -For instance, for this type and invocation: +### Selection and result -```csharp -public class C : IEnumerable -{ - public C(T2 t2) { ... } -} +Candidates from every bound and unbound type in the group form one set. A bound non-generic type is not tried before unbound generic types. Type-group lookup deliberately preserves types of different arities for one later binding operation, just as method-group lookup preserves methods for overload resolution. -IEnumerable l = new C(5); -``` +Normal overload resolution selects the best constructor using substituted parameter types. For better-function-member rules, a constructor of a generic or non-generic type is treated like a generic or non-generic method, respectively; the containing type's parameters and inferred arguments stand in for method type parameters and arguments. -Type inference would proceed as if with this method and invocation: +No applicable candidate or no unique best candidate is a binding-time error. The result type is the type containing the selected constructor, with inferred arguments substituted. Selecting a struct's implicit default constructor produces the default value of that constructed struct type. -```csharp -C M(T2 t2); +Object and collection initializer contents are processed after constructor selection and do not contribute inference bounds. Dynamically bound object creation continues to require a singleton group containing a bound class or struct type. -IEnumerable l = M(5); -``` +## Compatibility + +- An accessible same-name generic type can add candidates to an existing object creation, changing the selected constructor or making the expression ambiguous. +- Type-group lookup can find same-arity types that previous arity-specific lookup did not consider together, making lookup ambiguous before constructor selection. +- Target-derived bounds can change an argument-only inference result, allow a new candidate, or conflict with argument-derived bounds. +- Adding a same-name generic type can make dynamic construction invalid because dynamic construction requires a singleton bound type group. + +## Alternatives +[alternatives]: #alternatives + +### Phased fallback + +Constructor binding could try an existing bound type first and consider unbound generic types only if that produces no applicable constructor. This would preserve more existing selections, but it would discard candidates from a lookup result that deliberately groups types across arities. This proposal instead performs one selection over the complete group. + +## Open questions +[open]: #open-questions + +### Dynamic inference + +Should dynamically bound object creation ever support inference for constructors of generic types? The current design does not. -If type inference succeeds, and the inferred type arguments satisfy their constraints, and the constructor is applicable when the type arguments are applied to its containing generic type, then the constructor is a candidate. +### Initializer bounds -Overload resolution then proceeds as normal between the resulting candidate constructors. +Should object or collection initializer contents ever contribute inference bounds? The current design selects a constructor first.