meetings/working-groups/extensions/extensions-lookup.md
There are two parts to this proposal:
The proposal here is to look at extension<extensionTypeParameters>(receiverParameter) like a method signature,
and apply current type inference
and receiver applicability rules to it, given the type of a receiver.
The type inference step infers the extension type parameters (if possible).
The applicability step tells us whether the extension works with the given receiver,
using the applicability rules of this parameters.
This can be applied both when the receiver is an instance or when it is a type.
Re-using the existing type inference and conversion algorithm solves the variance problem we'd discussed in LDM.
It makes this scenario work as desired:
IEnumerable<string>.M();
static class E
{
extension(IEnumerable<object>)
{
public static void M() { }
}
}
Note: this change was made in the new extensions feature branch.
Decision: we're resolving new instance extension members exactly like classic extension methods.
The above should bring the behavior of new extensions very close to classic extensions.
But there is still a small gap with the current implementation of classic extension methods,
when arguments beyond the receiver are required for type inference of the type parameters
on the extension container.
The spec for classic extension methods specifies 2 phases (find candidates compatible with the receiver, then complete the overload resolution), but the implementation only has 1 phase (find all candidates and do overload resolution with all the arguments including one for the receiver value).
Example we had discussed:
public class C
{
public void M(I<string> i, out object o)
{
i.M(out o); // infers E.M<object>
i.M2(out o); // error CS1503: Argument 1: cannot convert from 'out object' to 'out string'
}
}
public static class E
{
public static void M<T>(this I<T> i, out T t) { t = default; }
extension<T>(I<T> i)
{
public void M2(out T t) { t = default; }
}
}
public interface I<out T> { }
We have a few options:
Note: depending on this choice, still need to solve how to mix classic and new extension methods in invocation and function type scenarios.
If we choose to do 1-phase lookup for invocation scenarios, we would:
The transformation at step 2 would take a method like the following:
static class E
{
extension<extensionTypeParameters>(receiverParameter)
{
void M<methodTypeParameters>(methodParameters);
}
}
and produce a signature like this:
static void M<extensionTypeParameters, methodTypeParameters>(this receiverParameter, methodParameters);
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.
Note: This approach solve the mixing question.
Decision: we're going for maximum compatibility between new instance extension methods and classic extension methods.
We previously concluded that we should prefer more specific extensions members.
But classic extension methods don't follow that.
Instead they use betterness rules (better function member).
We remove less specific applicable candidates of instance methods (type-like behavior)
new Derived().M(new Derived()); // Derived.M
public class Base
{
public void M(Derived d) { }
}
public class Derived : Base
{
public void M(Base b) { }
}
But we rely on betterness for classic extension methods (parameter-like behavior)
"".M(); // E.M(string)
public static class E
{
public static void M(this object o) { }
public static void M(this string s) { }
}
"".M(""); // ambiguous
public static class E
{
public static void M(this object o, string s) { }
public static void M(this string s, object o) { }
}
Which should we do for new extension methods?
We have competing goals:
Options for methods:
Gather candidates (no applicability involved) Pruning more candidates:
Note: the other pruning steps in overload resolution not sdon't apply to extension receiver parameter scenarios (RemoveDelegateConversionsWithWrongReturnType, RemoveCallingConventionMismatches, RemoveMethodsNotDeclaredStatic)
Decision: match what we do for instance methods
Do we want the same semantics for static extension methods (ie. we pretend like we have a receiver/value of the given type) or do we want some new semantics?
I assume that we want the old semantics.
This also makes it clear what to expect in a "Color Color" scenario, where we don't know whether the receiver is an instance or static.
Decision: match what we do for instance/static methods as long as it makes sense. In particular, the "prefer more specific" step won't apply to properties either. We'll want to revisit the details of betterness we want for properties. We're okay keeping the betterness step after the member kind determination, for now.
We have similar questions for extension properties.
We're going to cover three questions:
Yes, we'd previously agree that we want to prefer more specific members.
_ = "".P; // should pick E(string).P
public static class E
{
extension(object o)
{
public int P => throw null;
}
extension(string s) // more specific parameter type
{
public int P => 0;
}
}
If we try to follow the behavior of regular instance or static methods, then the resolution of extension properties should prune based on static/instance mismatch:
_ = 42.P;
static class E1
{
extension(int i)
{
public int P => 0;
}
}
static class E2
{
extension(int)
{
public static int P => throw null;
}
}
_ = int.P;
static class E1
{
extension(int i)
{
public int P => throw null;
}
}
static class E2
{
extension(int)
{
public static int P => 0;
}
}
IEnumerable<C2> iEnumerableOfC2 = null;
_ = iEnumerableOfC2.P; // should we prefer IEnumerable<C1> because it is a better conversion? (parameter-like behavior)
public static class E
{
extension(IEnumerable<C1> i)
{
int P => 0;
}
extension(IEnumerable<object> i)
{
int P => throw null;
}
}
public class C1 { }
public class C2 : C1 { }
_ = IEnumerable<C2>.P; // should we prefer IEnumerable<C1> because it is a better conversion? (parameter-like behavior)
public static class E
{
extension(IEnumerable<C1>)
{
static int P => 0;
}
extension(IEnumerable<object>)
{
static int P => throw null;
}
}
Should both extensions be applicable both when the receiver is an instance or a type?
If yes, should we have some preference between those two?
If we follow the parameter-like behavior of classic extension methods, then we'd probably want more better member rules:
_ = 42.P;
public static class E
{
extension<T>(T t)
{
public int P => throw null;
}
extension(int i) // non-generic, so better function member
{
public int P => 0;
}
}
_ = 42.P;
public static class E
{
extension(in int i)
{
public int P => throw null;
}
extension(int i) // better parameter-passing mode
{
public int P => 0;
}
}
But those betterness rules don't necessarily feel right when it comes to static extension methods:
int.M2();
public static class E1
{
extension(in int i)
{
public static void M() { }
}
}
public static class E2
{
extension(int)
{
public static void M() { }
}
}
Following last LDM's decision to use old semantics for new extension methods, we have to find a new way to resolve properties and methods together.
Previously, we would only gather candidates from compatible extensions, then we would decide the winning member kind and proceed with resolving (either overload resolution for methods, or picking the single property).
Now that we're gathering all candidates (without regards to compatibility of extensions), we're thinking to delay the determination of the member kind.
The process that we've brainstormed:
If all the remaining candidates are methods, the member kind is method and we resolve to the best method.
If the only remaining candidate is a property, the member kind is property and we resolve to that property.
Otherwise we have an ambiguity.
Note: I don't think there's a scenario for removing lower priority members based on ORPA here.
The following example illustrates the relevance of step 3 above, when we have a method and a property, but one has a static/instance mismatch.
object.M();
static class E1
{
extension(object)
{
public static string M() => throw null;
}
}
static class E2
{
extension(object o)
{
public string M() => throw null;
}
}
The following example illustrates the relevance of step 3 above, when we have a method and a property, but one is more specific.
The problem is that we've decided not to apply the "more specific" pruning step to extension methods, for compatibility with classic extension methods.
string.M();
static class E1
{
extension(string)
{
public static string M() => throw null;
}
}
static class E2
{
extension(object)
{
public static System.Action M => throw null;
}
}
Note: the determination of function types starts with the applicable candidates, with candidates pruned, but some of the pruning rules weren't applicable (static/instance mismatch).
I assume we'll want all possible applicable pruning steps to apply:
var x = C.M; // binds to static method
public class C { }
public static class E1
{
extension(object)
{
public static void M() { }
}
extension(object o)
{
public void M(int i) { }
}
}