|
| 1 | +There are two parts to this proposal: |
| 2 | +1. adjust how we find compatible substituted extension containers |
| 3 | +2. align with current implementation of extension methods |
| 4 | + |
| 5 | +# Finding a compatible substituted extension container |
| 6 | + |
| 7 | +The proposal here is to look at `extension<extensionTypeParameters>(receiverParameter)` like a method signature, |
| 8 | +and apply current [type inference](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#1263-type-inference) |
| 9 | +and [receiver applicability](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#128103-extension-method-invocations) rules to it, given the type of a receiver. |
| 10 | + |
| 11 | +The type inference step infers the extension type parameters (if possible). |
| 12 | +The applicability step tells us whether the extension works with the given receiver, |
| 13 | +using the applicability rules of `this` parameters. |
| 14 | + |
| 15 | +This can be applied both when the receiver is an instance or when it is a type. |
| 16 | + |
| 17 | +Re-using the existing type inference algorithm solves the variance problem we'd discussed in LDM. |
| 18 | +It makes this scenario work as desired, because type inference is smarter than the implemented algorithm for extensions: |
| 19 | +```cs |
| 20 | +IEnumerable<string>.M(); |
| 21 | + |
| 22 | +static class E |
| 23 | +{ |
| 24 | + extension(IEnumerable<object>) |
| 25 | + { |
| 26 | + public static void M() { } |
| 27 | + } |
| 28 | +} |
| 29 | +``` |
| 30 | + |
| 31 | +# Aligning with implementation of classic extension methods |
| 32 | + |
| 33 | +The above should bring the behavior of new extensions very close to classic extensions. |
| 34 | +But there is still a small gap with the current implementation of classic extension methods, |
| 35 | +when arguments beyond the receiver are required for type inference of the type parameters |
| 36 | +on the extension container. |
| 37 | +The spec for classic extension methods specifies 2 phases (find candidates compatible with the receiver, then complete the overload resolution), |
| 38 | +but the implementation only has 1 phase (find all candidates and do overload resolution with all the arguments including one for the receiver value). |
| 39 | + |
| 40 | +Example we had [discussed](https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-10-02.md#extensions): |
| 41 | +```cs |
| 42 | +public class C |
| 43 | +{ |
| 44 | + public void M(I<string> i, out object o) |
| 45 | + { |
| 46 | + i.M(out o); // infers E.M<object> |
| 47 | + i.M2(out o); // error CS1503: Argument 1: cannot convert from 'out object' to 'out string' |
| 48 | + } |
| 49 | +} |
| 50 | +public static class E |
| 51 | +{ |
| 52 | + public static void M<T>(this I<T> i, out T t) { t = default; } |
| 53 | + extension<T>(I<T> i) |
| 54 | + { |
| 55 | + public void M2(out T t) { t = default; } |
| 56 | + } |
| 57 | +} |
| 58 | +public interface I<out T> { } |
| 59 | +``` |
| 60 | + |
| 61 | +My proposal is that the implementation continue to diverge from the spec: instead of doing 2-phase lookup |
| 62 | +(as described in the section above, where we find compatible substituted extension containers, then find the candidate members in those) |
| 63 | +we could do a 1-phase lookup. We would only do this in invocation scenarios. |
| 64 | + |
| 65 | +For such invocation scenarios: |
| 66 | +1. we collect all the candidate methods (both classic extension methods and new ones, without excluding any extension containers) |
| 67 | +2. we combine all the type parameters and the parameters into a single signature |
| 68 | +3. we apply overload resolution to the resulting set |
| 69 | + |
| 70 | +The transformation at step2 would take a method like the following: |
| 71 | +```cs |
| 72 | +static class E |
| 73 | +{ |
| 74 | + extension<extensionTypeParameters>(receiverParameter) |
| 75 | + { |
| 76 | + void M<methodTypeParameters>(methodParameters); |
| 77 | + } |
| 78 | +} |
| 79 | +``` |
| 80 | +and produce a signature like this: |
| 81 | +``` |
| 82 | +static void M<extensionTypeParameters, methodTypeParameters>(this receiverParameter, methodParameters); |
| 83 | +``` |
| 84 | + |
| 85 | +Note: for static scenarios, we would play the same trick as in the above section, where we take a type/static receiver and use it as an argument. |
| 86 | + |
| 87 | +# Recap |
| 88 | + |
| 89 | +If we accepted both parts of the proposal: |
| 90 | +- `instance.Method(...)` would behave exactly the same whether `Method` is a classic or new extension method |
| 91 | +(from an implementation perspective) |
| 92 | +- `Type.Method(...)` would behave exactly like the instance scenario |
| 93 | +- other scenarios all use the new resolution method where we figure out the compatible substituted extension containers, then collect candidates |
| 94 | +- `instance.Property` and `Type.Property` |
| 95 | +- `instance[...]` |
| 96 | +(we first figure out the compatible substituted extension container, then do overload resolution with the candidate indexers) |
| 97 | +- const, nested type, operators, ... |
| 98 | + |
| 99 | + |
0 commit comments