We need to revise the preference of extension types over extension methods (should mix and disambiguate duplicates if needed instead)
static class Extensions
{
public static X ToX<Y>(this IEnumerable<Y> values) => ...
}
implicit extension ImmutableArrayExtensions<Y> for ImmutableArray<Y>
{
public X ToX() => ...
}
// or reverse:
static class Extensions
{
public static X ToX<Y>(this ImmutableArray<Y> values) => ...
}
implicit extension IEnumerableExtensions<Y> for IEnumerable<Y>
{
public X ToX() => ...
}In this world, i have existing extensions and i add the new features because it feels like the right way to do modern C#. And either has a problem depending on a priority system picking the "worse" overload. I want these mixed. Just as if i had done:
static class Extensions
{
public static X ToX<Y>(this ImmutableArray<Y> values) => ...
public static X ToX<Y>(this IEnumerable<Y> values) => ...
}Confirm what happens when we have different kinds of members:
var c = new C();
c.M(ImmutableArray.Create(1, 2, 3)); // What should happen?
class C
{
}
public static class CExt
{
public static void M(this C c, IEnumerable<int> e) => ...
}
public implicit extension E1 for C
{
public Action<ImmutableArray<int>> M => ...
}class TableIDoNotOwn : IEnumerable<Item> { }
static class IEnumerableExtensions
{
public int Count<T>(this IEnumerable<T> t);
}
implicit extension MyTableExtensions for TableIDoNotOwn
{
public int Count { get { ... } }
}
// What happens here?
var v = table.Count; // Let's get a read from LDMCurrently, readonly members are disallowed in extensions. This means that
a ref readonly variable cannot be used as the instance argument for an extension member without cloning.
We should consider allowing readonly members.
var s = new S() { field = 42 };
M(in s);
System.Console.Write(s.field); // we can observe whether the receiver was cloned or not
void M(in S s)
{
s.M();
}
public struct S
{
public int field;
public void Increment() { field++; }
}
public implicit extension E for S
{
public void M() // readonly modifier is currently disallowed
{
this.Increment();
}
}
Should we disambiguate when signatures match?
var x = new C().M; // ambiguous
new C().M(); // ambiguous once we mix all extensions
class C { }
implicit extension E for C
{
public static void M() { }
}
static class Extensions
{
public static void M(this C c) { }
}We'll need to update "extension member lookup section" and "using static directives" to allow the following two scenarios:
using static C;
using static D;
Nested1.M(); // Nested1 should be in scope too (as if it were declared as a static member of C)
Nested2.M();
class C { }
implicit extension E for C
{
public class Nested1 { public static void M() { } }
}
class D { public class Nested2 { public static void M() { } }}using static Extension;
// M and Nested should be in scope
explicit extension Extension for object
{
public static void M() { }
public class Nested { }
}Many sections of the spec need to consider the kind of type. Consider a few examples:
- the spec for a conditional element access
P?[A]Bconsiders whetherPis a nullable value type. So it will need to handle the case wherePis an instance of an extension type on a nullable value type, - the spec for an object creation considers whether the type is a value_type, a type_parameter, a class_type or a struct_type,
- the spec for satisfying constraints also consider what kind of type we're dealing with.
It may be possible to address all those cases without changing each such section of the spec, but rather by adding general rules ("an extension on a class type is considered a class type" or some such).
The this access section needs to be updated to handle type parameters.
The implementation details section needs to be updated to explain how we capture the receiver when the type parameter is a reference type.
var o = new object();
o.M(o = null);
implicit extension E<T> for T
{
// emitted as `void M(ref modreq(ExtensionAttribute) T, object)`
void M(object x) { this.ToString(); }
}It should be done in a way that allows switching between the extension type and the underlying type without binary break.
It should allow for using type parameters as type arguments: void M<T>(E<T> e).
It should allow for using extension types as type arguments without violating type parameter constraints: C<E> with class C<T> where T : struct, I { }.
The current proposal is to use an attribute with a string representing the type with extensions un-erased.
For example: void M(E) would be emitted as void M([Attribute("E")] UnderlyingType).
The serialization format would be the same as the one used for typeof in attributes, but with support for type parameters (using !N and !!N notation from ECMA 335).
II.9 Generics Within a generic type definition, its generic parameters are referred to by their index. Generic parameter zero is referred to as !0, generic parameter one as !1, and so on. Similarly, within the body of a generic method definition, its generic parameters are referred to by their index; generic parameter zero is referred to as !!0, generic parameter one as !!1, and so on.
Note: the serialization format does not support function pointers at the moment. Tracked by dotnet/roslyn#48765
The attribute would also encode the tuple names, dynamic, native integer and nullability information for the type with extensions un-erased.
For example: void M(E<dynamic>) with C<(dynamic a, dynamic b)> as the underlying type for E<dynamic> would be emitted as
void M([Attribute("E<object>"", TupleNames = "..."}] [... existing attributes for dynamic and tuple names ... ] C<ValueTuple<object, object>>).
Should we instead extend support for type parameters in typeof in attributes (including runtime/reflection),
and use a Type parameter in the attribute constructor?
The current rules are pretty strict:
a possible type substitution on the type parameters of
Xyields underlying typeU, a base type ofUor an implemented interface ofU.
Whereas extension method invocation says:
An implicit identity, reference or boxing conversion exists from expr to the type of the first parameter of Mₑ.
We'll likely want to allow variance such as object vs. dynamic, tuple names, nullability.
Also, we may want to allow for variance (see example below). But this would involve considering conversion when evaluating compatibility.
IEnumerable<string>.M();
implicit extension E for IEnumerable<object>
{
public static void M() { }
}Should we allow top-level nullability on underlying type?
implicit extension E for object? { }What is the nullability of this within an extension member?
implicit extension E for object
{
public void M()
{
this.ToString(); // is this safe?
}
}object? x = null;
x.M(); // is this safe?C<object>.M(); // warn?
implicit extension E for C<object?>
{
public static void M() { }
}Need to spec why extension properties are not found during lookup for attribute properties, or explicitly disallow them
From WG discussion, we'd allow attributes on extensions types, using the AttributeTargets.Class target, since extensions are emitted as static classes.
We don't resolve implicit extension members in usings, due to cycles.
This needs to be investigated further to disentangle whether all cycles are implementation-specific
or some are inherent to the language feature.
Adjust scoping rules so that type parameters are in scope within the 'for':
implicit extension E<T> for T { }explicit extension R for U { }
R r = default;
object o = r; // what conversion is that? if R doesn't have `object` as base type. What about interfaces?
Should allow conversion operators. Extension conversion is useful.
Example: from int to string (done by StringExtension).
But we should disallow user-defined conversions from/to underlying type
or inherited extensions, because a conversion already exists.
Conversion to interface still disallowed.
Could we make the conversion to be explicit identity conversion instead
of implicit?
User-defined conversion should be allowed, except where it conflicts with a built-in
conversion (such as with an underlying type).
Do we want to allow constructors and required properties?
Do we want to allow operators?
We need to review Method type inference: 7.5.2.9 Lower-bound interfaces, to see whether any updates are needed.
We want to allow the following and decide the inferred type:
var c = new C();
var e = (E)c; // E is an extension type
M(c, e); // M<C> or M<E>?
void M<T>(T t1, T t2) { }Should we have a naming convention like Extension suffixes? (DataObjectExtension)
We need to scan through all pattern-based rules for known members to consider whether to include extension type members. If extension methods were already included, then we should certainly include extension type methods. Otherwise, we should consider it.
Deconstructin deconstruction and patternsGetEnumerator,CurrentandMoveNextinforeach- CollectionBuilder in collection expressions
Addin collection initializersGetPinnableReferencein fixed statementsGetAwaiterinawaitexpressionsDisposeinusingstatements
Will need to check with the WG as I don't recall the reasoning for this.
The static+this codegen strategy should work for ref structs and classic extension methods are allowed on ref structs.
The purpose of "extensions" is to augment or adapt existing types to new scenarios, when those types are not under your control, or where changing them would negatively impact other uses of them. The adaptation can be in the form of adding new function members as well as implementing additional interfaces.
explicit extension CustomerExtension for byte[] : PersonExtension { ... }
implicit extension EnumExtension for Enum { ... }
Explicit extensions address two main classes of scenarios: "augmentation" scenarios (fit an existing value with a new member) and "adaptation" scenarios (fit an existing value to an existing interface).
In addition, implicit extensions would provide for additional kinds of extension members beyond today's extension methods, and potentially for "extension interfaces" in the future.
Augmentation scenarios allow existing values to be seen through the lens of a "stronger" type - an extension that provides additional function members on the value.
Adaptation scenarios allow existing values to be adapted to existing interfaces,
where an extension provides details on how the interface members are implemented.
Those are not out-of-scope for this document.
The syntax for extensions is as follows:
type_declaration
| extension_declaration // add
| ...
;
extension_declaration
: extension_modifier* ('implicit' | 'explicit') 'extension' identifier type_parameter_list? ('for' type)? type_parameter_constraints_clause* extension_body
;
extension_body
: '{' extension_member_declaration* '}'
;
extension_member_declaration
: constant_declaration
| field_declaration
| method_declaration
| property_declaration
| event_declaration
| indexer_declaration
| operator_declaration
| type_declaration
;
extension_modifier
| 'partial'
| 'unsafe'
| 'static'
| 'protected'
| 'internal'
| 'private'
| 'file'
;An extension type (new kind of type) is declared by a extension_declaration.
The extension type does not inherit members from its underlying type
(which may be sealed or a struct), but
the lookup rules are modified to achieve a similar effect (see below).
There is an identity conversion between an extension and its underlying type.
An extension type satisfies the constraints satisfied by its underlying type (see section on constraints).
The extension_underlying_type type may not be dynamic, a pointer,
a ref struct type, a ref type or an extension.
The underlying type may not include an interface_type_list (this is part of the Interface Phase).
The extension type must be static if its underlying type is static.
When a partial extension declaration includes an underlying type specification, that underlying type specification shall reference the same type as all other parts of that partial type that include an underlying type specification. It is a compile-time error if no part of a partial extension includes an underlying type specification.
It is a compile-time error if the underlying type differs amongst all the parts of an extension declaration.
It is a compile-time error if the implicit or explicit modifiers differ amongst
all the parts of an extension declaration.
The permitted modifiers on an extension type are partial,
unsafe, static, file and the accessibility modifiers.
A static extension shall not be instantiated, shall not be used as a type and shall
contain only static members.
The standard rules for modifiers apply (valid combination of access modifiers, no duplicates).
Extension types may not contain instance fields (either explicitly or implicitly).
When a partial extension declaration includes an accessibility specification,
that specification shall agree with all other parts that include an accessibility specification.
If no part of a partial extension includes an accessibility specification,
the type is given the appropriate default accessibility (internal).
An implicit extension type is an extension whose members can be found on the underlying type (or a value of the underlying type) when the extension is "in scope" and compatible with the underlying type (see extension member lookup section).
The underlying type must include all the type parameters from the implicit extension type.
We'll use "extends" for relationship to underlying/extended type (comparable to "inherits" for relationship to base type).
struct U { }
explicit extension X for U { }"X has underlying type U"
"X extends U"
We modify the accessibility constraints as follows:
The following accessibility constraints exist:
- [...]
- *The underlying type of an extension type shall be at least as accessible as the extension type itself.
Note those also apply to visibility constraints of file-local types (not yet specified).
The extension type members may not use the virtual, abstract, sealed, override modifiers,
or the protected access modifier.
Member methods may not use the readonly modifier.
The new modifier is allowed and the compiler will warn that you should
use new when shadowing.
An extension cannot contain a member declaration with the same name as the extension.
The existing rules for signatures apply.
Two signatures differing by an extension vs. its underlying type are considered to be the same signature.
Shadowing includes underlying type.
class U { public void M() { } }
explicit extension R for U { /*new*/ public void M() { } } // wins when dealing with an R
class U { public void M() { } }
implicit extension X for U { /*new*/ public void M() { } } // ignored in some cases
U u;
u.M(); // U.M (ignored X.M)
X x;
x.M(); // X.M
Existing rules for constants
apply (so duplicates or the static modifier are disallowed).
A field_declaration in an extension_declaration shall explicitly include a static modifier.
Otherwise, existing rules for fields apply.
Parameters with the this modifier are disallowed.
Otherwise, existing rules for methods apply.
In particular, a static method does not operate on a specific instance,
and it is a compile-time error to refer to this in a static method.
We modify the extension methods rules as follows:
[...] The first parameter of an extension method may have no modifiers other than this,
and the parameter type may not be a pointer or an extension type.
Auto-properties must be static (since instance fields are disallowed).
Existing rules for properties apply.
In particular, a static property does not operate on a specific instance,
and it is a compile-time error to refer to this in a static property.
extension Extension : UnderlyingType
{
class NestedType { }
}
class UnderlyingType { }
UnderlyingType.NestedType x = null; // okay
TL;DR: An extension satisfies the constraints satisfied by its underlying type. Extensions cannot be used as type constraints.
We modify the rules on satisfying constraints as follows:
Whenever a constructed type or generic method is referenced, the supplied type arguments
are checked against the type parameter constraints declared on the generic type or method.
For each where clause, the type argument A that corresponds to the named type parameter
is checked against each constraint as follows:
- If the constraint is a class type, an interface type, or a type parameter,
let
Crepresent that constraint with the supplied type arguments substituted for any type parameters that appear in the constraint. To satisfy the constraint, it shall be the case that typeAis convertible to typeCby one of the following:- An identity conversion
- An implicit reference conversion
- A boxing conversion, provided that type
Ais a non-nullable value type. - An implicit reference, boxing or type parameter conversion from a type parameter
AtoC.
- If the constraint is the reference type constraint (
class), the typeAshall satisfy one of the following:Ais an interface type, class type, delegate type, array type or the dynamic type.Ais a type parameter that is known to be a reference type.- *
Ais an extension type with an underlying type that satisfies the reference type constraint.
- If the constraint is the value type constraint (
struct), the typeAshall satisfy one of the following:Ais astructtype orenumtype, but not a nullable value type.Ais a type parameter having the value type constraint.- *
Ais an extension type with an underlying type that satisfies the value type constraint.
- If the constraint is the constructor constraint
new(), the typeAshall not beabstractand shall have a public parameterless constructor. This is satisfied if one of the following is true:Ais a value type, since all value types have a public default constructor.Ais a type parameter having the constructor constraint.Ais a type parameter having the value type constraint.Ais aclassthat is not abstract and contains an explicitly declared public constructor with no parameters.Ais notabstractand has a default constructor.- *
Ais an extension type with an underlying type that satisfies the constructor constraint.
A compile-time error occurs if one or more of a type parameter’s constraints are not satisfied by the given type arguments.
By the existing rules on type parameter constraints extensions are disallowed in constraints (an extension is neither a class or an interface type).
where T : Extension // error
TODO Does this restriction on constraints cause issues with structs?
Types and aliases may not be called "extension".
TL;DR: For certain syntaxes (member access, element access), we'll fall back to an implicit extension member lookup.
No changes to simple names rules are needed. Member lookup on a type or value of extension type includes accessible members from its extended type.
TL;DR: After doing an unsuccessful member lookup in a type, we'll perform an extension member lookup for non-invocations or attempt an extension invocation for invocations.
We modify the member access rules as follows:
The member_access is evaluated and classified as follows:
-
... the result is that namespace.
-
... the result is that type constructed with the given type arguments.
-
If
Eis classified as a type, ifEis not a type parameter, and if a member lookup ofIinEwithKtype parameters produces a match, thenE.Iis evaluated and classified as follows:Note: When the result of such a member lookup is a method group and
Kis zero, the method group can contain methods having type parameters. This allows such methods to be considered for type argument inferencing. end note- If
Iidentifies a type, then the result is that type constructed with any given type arguments. - If
Iidentifies one or more methods, then the result is a method group with no associated instance expression. - If
Iidentifies a static property, then the result is a property access with no associated instance expression. - If
Iidentifies a static field:- If the field is readonly and the reference occurs outside the static constructor
of the class or struct in which the field is declared, then the result is a value,
namely the value of the static field
IinE. - Otherwise, the result is a variable, namely the static field
IinE.
- If the field is readonly and the reference occurs outside the static constructor
of the class or struct in which the field is declared, then the result is a value,
namely the value of the static field
- If
Iidentifies a static event:- If the reference occurs within the class or struct in which the event is declared,
and the event was declared without event_accessor_declarations,
then
E.Iis processed exactly as ifIwere a static field. - Otherwise, the result is an event access with no associated instance expression.
- If the reference occurs within the class or struct in which the event is declared,
and the event was declared without event_accessor_declarations,
then
- If
Iidentifies a constant, then the result is a value, namely the value of that constant. - If
Iidentifies an enumeration member, then the result is a value, namely the value of that enumeration member. - *
Otherwise,E.Iis an invalid member reference, and a compile-time error occurs.
- If
-
*If
E.Iis not invoked andEis classified as a type, ifEis not a type parameter, and if an extension member lookup ofIinEwithKtype parameters produces a match, thenE.Iis evaluated and classified as follows:
... -
If
Eis a property access, indexer access, variable, or value, the type of which isT, and a member lookup ofIinTwithKtype arguments produces a match, thenE.Iis evaluated and classified as follows:
... -
*If
E.Iis not invoked andEis a property access, indexer access, variable, or value, the type of which isT, and an extension member lookup ofIinTwithKtype arguments produces a match, thenE.Iis evaluated and classified as follows:
... -
Otherwise, an attempt is made to process
E.Ias an *extension invocation. If this fails,E.Iis an invalid member reference, and a binding-time error occurs.
Note: the path to extension invocation from this section is only for empty results from member lookup.
Note: We allow static lookups on type parameters only for members that are static virtual members. Since extensions members are not virtual, we don't allow static extension lookups on type parameters either.
void M<T>(T t)
{
T.MStatic() // disallow
t.MInstance() // allow
}
implicit extension E for U
{
public static void MStatic() { }
public void MInstance() { }
}
TL;DR: Instead of falling back to "extension method invocation" directly, we'll now fall back to "extension invocations" which replaces it.
We modify the method invocations rules as follows:
[...]
- If the resulting set of candidate methods is empty, then further processing along the following steps are abandoned, and instead an attempt is made to process the invocation as *an extension invocation. If this fails, then no applicable methods exist, and a binding-time error occurs. [...]
Note: the change to the Base Types section also affects the method invocation rules:
The set of candidate methods is reduced to contain only methods from the most derived types: For each method
C.Fin the set, whereCis the type in which the methodFis declared, all methods declared in a base type ofCare removed from the set.
For example:
E.Method(); // picks `E.Method` over `C.Method`
static class C
{
public static void Method() => throw null;
}
static explicit extension E for C
{
public static void Method() { } // picked
}We replace the extension method invocations rules with the following:
In an invocation of one of the forms
«Type» . «identifier» ( )
«Type» . «identifier» ( «args» )
«Type» . «identifier» < «typeargs» > ( )
«Type» . «identifier» < «typeargs» > ( «args» )
«expr» . «identifier» ( )
«expr» . «identifier» ( «args» )
«expr» . «identifier» < «typeargs» > ( )
«expr» . «identifier» < «typeargs» > ( «args» )if the normal processing of the invocation finds no applicable methods,
an attempt is made to process the construct as an invocation of an extension type member
or an extension method.
If expr or any of te «args» has compile-time type dynamic,
extensions (extension type members or extension methods) will not apply.
This succeeds if we find either:
- for the
Typecase, an substituted compatible implicit extension typeXforTypeso that the corresponding invocation can take place:
X . «identifier» ( )
X . «identifier» ( «args» )
X . «identifier» < «typeargs» > ( )
X . «identifier» < «typeargs» > ( «args» )- for the
exprcase where the expression has typeType, a substituted compatible implicit extension typeXforTypeso that the corresponding invocation can take place:
((X)expr) . «identifier» ( )
((X)expr) . «identifier» ( «args» )
((X)expr) . «identifier» < «typeargs» > ( )
((X)expr) . «identifier» < «typeargs» > ( «args» )- for the
exprcase, the best type_nameCso that the corresponding static extension method invocation can take place:
C . «identifier» ( «expr» )
C . «identifier» ( «expr» , «args» )
C . «identifier» < «typeargs» > ( «expr» )
C . «identifier» < «typeargs» > ( «expr» , «args» )[Extension method eligibility remains unchanged]
The search proceeds as follows:
- Starting with the closest enclosing type declaration, continuing with each type declaration,
then continuing with each enclosing namespace declaration, and ending with
the containing compilation unit, successive attempts are made:
- If the given type, namespace or compilation unit directly contains extension types or methods, those will be considered first.
- If namespaces imported by using-namespace directives in the given namespace or compilation unit directly contain extension types or methods, those will be considered second.
- First, try extension types:
- Check which extension types in the current scope are compatible with the given underlying type
Typeand collect resulting compatible substituted extension types. - Perform member lookup for
identifierin each compatible substituted extension type. (note this takes into account that the member is invoked) (note this doesn't include members from the underlying type) - Merge the results
- Next, members that are hidden by other members are removed from the set.
(note: "base types" means "underlying type" for extension types) - Next, less specific extension members are removed if they are "hidden" by more specific extension members.
For every member
X.Min the set, whereXis the type in which the memberMis declared, if no member in the set has a containing typeYthat is more specific thanXthen the following rules are applied:- If
Mis a method, then all non-method members declared in a less specific type thanXare removed from the set. - Otherwise, all members declared in a less specific type than
Xare removed from the set.
- If
- Finally, having removed hidden and less specific members:
- If the set is empty, proceed to extension methods below.
- If the set consists of a single member that is not a method, then:
- If it is a value of a delegate_type, the invocation_expression is evaluated as a delegate invocation.
- If it is a value of a function_pointer_type, the invocation_expression is evaluated as a function pointer invocation.
- If it is a value of a type
dynamic, the invocation_expression is evaluated as a dynamic member invocation.
- If the set contains only methods, we remove all the methods that are not
accessible or applicable (see "method invocations").
- If the set is empty, proceed to extension methods below.
- Otherwise, overload resolution is applied to the candidate methods:
- If a single best method is found, the invocation_expression is evaluated as the invocation of this method.
- If no single best method is found, a compile-time error occurs.
- Check which extension types in the current scope are compatible with the given underlying type
- Next, try extension methods (only for the
exprcase):- Check which extension methods in the current scope are eligible.
- If the set is empty, proceed to the next enclosing scope.
- Otherwise, overload resolution is applied to the candidate set.
- If a single best method is found, the invocation_expression is evaluated as a static method invocation.
- If no single best method is found, a compile-time error occurs.
- Check which extension methods in the current scope are eligible.
- Proceed to the next enclosing scope
- If no extension type member or extension method is found to be suitable for the invocation in any enclosing scope, a compile-time error occurs.
The preceding rules mean:
- that instance methods take precedence over extension methods,
- that extension type members available in a given namespace take precedence over extension methods in that namespace,
- that extension type members available in inner namespace declarations take precedence over extension type members available in outer namespace declarations,
- that extension methods available in inner namespace declarations take precedence over extension methods available in outer namespace declarations,
- that extension type members declared directly in a namespace take precedence over extension type members imported into that same namespace with a using namespace directive,
- and that extension methods declared directly in a namespace take precedence over extension methods imported into that same namespace with a using namespace directive.
TL;DR: For non-extension types, we'll fall back to an implicit extension member lookup. For extension types, we include indexers from the underlying type.
We modify the element access rules as follows:
An element_access is dynamically bound if [...] If the primary_no_array_creation_expression of an element_access is a value of an array_type, the element_access is an array access. Otherwise, the primary_no_array_creation_expression shall be a variable or value of a class, struct, interface, *or extension type that has one or more indexer members, in which case the element_access is an indexer access. *Otherwise, the primary_no_array_creation_expression shall be a variable or value of a class, struct, or interface type that has no indexer members, in which case the element_access is an extension indexer access.
We modify the indexer access rules as follows:
For an indexer access, the primary_no_array_creation_expression of the element_access shall be a variable or value of a class, struct, interface, *or extension type, and this type shall implement one or more indexers that are applicable with respect to the argument_list of the element_access.
The binding-time processing of an indexer access of the form P[A], where P is a primary_no_array_creation_expression
of a class, struct, interface, *or extension type T, and A is an argument_list, consists of the following steps:
- The set of indexers provided by
Tis constructed. The set consists of all indexers declared inTor a base type ofTthat are not override declarations and are accessible in the current context. - The set is reduced to those indexers that are applicable and not hidden by other indexers. (note: "base types" means "underlying type" for extension types) [...]
- *If the resulting set of candidate indexers is empty, then further processing along the following steps are abandoned, and instead an attempt is made to process the indexer access as an extension indexer access. If this fails, then no applicable indexers exist, and a binding-time error occurs.
If the resulting set of candidate indexers is empty, then no applicable indexers exist, and a binding-time error occurs.- The best indexer of the set of candidate indexers is identified using the overload resolution rules. If a single best indexer cannot be identified, the indexer access is ambiguous, and a binding-time error occurs.
- [...]
new C()[42]; // binds to C.this[int] from instance type
new E()[42]; // binds to C.this[int] from underlying type
new C()[""]; // binds to C.this[string] from instance type
new E()[""]; // binds to E.this[string] (extension indexer access)
class C
{
public int this[string s] => throw null;
public int this[int i] => throw null;
}
implicit extension E for C
{
public new int this[string s] => throw null;
}TODO write this section, including preference for more specific extension indexers
In an element access of one of the forms
«expr» [ ]
«expr» [ «args» ]if the normal processing of the element access finds no applicable indexers,
an attempt is made to process the construct as an extension indexer access.
If «expr» or any of the «args» has compile-time type dynamic, extension methods will not apply.
This succeeds if, given that «expr» has underlying type Type, we find
a substituted compatible implicit extension type X for Type
so that the corresponding element access can take place:
((X)expr) . «identifier» [ ]
((X)expr) . «identifier» [ «args» ]The search proceeds as follows:
-
If «expr» has an extension type,
Typeis the underlying type of that extension type. Otherwise,Typeis the compile-time type of «expr». -
Starting with the closest enclosing type declaration, continuing with each type declaration, then continuing with each enclosing namespace declaration, and ending with the containing compilation unit, successive attempts are made:
- If the given type, namespace or compilation unit directly contains extension types or methods, those will be considered first.
- If namespaces imported by using-namespace directives in the given namespace or compilation unit directly contain extension types or methods, those will be considered second.
- Check which extension types in the current scope are compatible with the given underlying type
Typeand collect resulting compatible substituted extension types. - The set of indexers is constructed from all indexers declared in each substituted extension type that are not override declarations and are accessible in the current context.
- Merge the results
- Next, members that are hidden by other members are removed from the set.
(note: "base types" means "underlying type" for extension types) - Next, members that are not applicable with respect to the given argument_list are removed from the set.
- Finally, having removed hidden and inapplicable members:
- If the set is empty, proceed to the next enclosing scope.
- Otherwise, overload resolution is applied to the candidate indexers:
- If a single best indexer is found, the element_access is evaluated as the invocation of either the get_accessor or the set_accessor of the indexer.
- If no single best indexer is found, a compile-time error occurs.
-
If no extension indexer is found to be suitable for the element access in any enclosing scope, a compile-time error occurs.
We modify the this access rules as follows:
A this_access has one of the following meanings:
- [...]
- *When this is used in a primary_expression within an instance method or instance accessor of an extension with an underlying type known to be a reference type, it is classified as a value. The type of the value is the instance type of the extension within which the usage occurs, and the value is a reference to the object for which the method or accessor was invoked.
- ***When this is used in a primary_expression within an instance method or instance accessor of an extension with an underlying type known to be a value type,
it is classified as a variable. The type of the variable is the instance type of the extension within which the usage occurs.
- If the method or accessor is not an iterator or async function, the
thisvariable represents the extension for which the method or accessor was invoked.- If the value type is a readonly struct, the
thisvariable behaves exactly the same as aninparameter of the struct type - Otherwise the
thisvariable behaves exactly the same as arefparameter of the value type
- If the value type is a readonly struct, the
- If the method or accessor is an iterator or async function, the
thisvariable represents a copy of the value for which the method or accessor was invoked, and behaves exactly the same as a value parameter of the value type.**
- If the method or accessor is not an iterator or async function, the
We'll start by disallowing base access
within extension types.
Casting seems an adequate solution to access hidden members: ((R)r2).M().
In the future, maybe base. could refer to underlying value.
TL;DR: Member lookup on an extension type includes members from its extended type and base types.
We modify the member lookup rules as follows:
For purposes of member lookup, a type T is considered to have the following base types:
- If
Tisobjectordynamic, thenThas no base type. - If
Tis an enum_type, the base types ofTare the class typesSystem.Enum,System.ValueType, andobject. - If
Tis a struct_type, the base types ofTare the class typesSystem.ValueTypeandobject. - If
Tis a class_type, the base types ofTare the base classes ofT, including the class typeobject. - If
Tis an interface_type, the base types ofTare the base interfaces ofTand the class typeobject. - If
Tis an array_type, the base types ofTare the class typesSystem.Arrayandobject. - If
Tis a delegate_type, the base types ofTare the class typesSystem.Delegateandobject. - *If
Tis an extension_type, the base types ofTare the extended type ofTand its base types.
Note: this allows method groups that contain members from the extension and the extended type together:
class U
{
public void M2() { }
}
explicit extension R : U
{
public void M2(int i) { }
void M()
{
M2(); // find `U.M2()`
}
}Note: this also affects what members are considered shadowed, so that we don't get an overload resolution ambiguity in a scenario like this:
class U
{
public void M2() { }
}
explicit extension R : U
{
public void M2() { } // warning: needs `new`
void M()
{
M2(); // find `R.M2()`, no ambiguity
}
}TL;DR: We can determine whether an implicit extension is compatible with a given underlying type and when successful this process yields one or more extension types we can use (including required substitutions).
An extension type X is compatible with given type U if:
Xis non-generic and its underlying type isU, a base type ofUor an implemented interface ofU- a possible type substitution on the type parameters of
Xyields underlying typeU, a base type ofUor an implemented interface ofU.
Note: it is possible for multiple substitutions on the type parameters of X to satisfy the condition above.
We call the resulting substituted types of X the "compatible substituted extension types".
#nullable enable
implicit extension Extension<T> for Underlying<T> where T : class { }
class Base<T> { }
class Underlying<T> : Base<T> { }
Base<object> b; // Extension<object> is a compatible extension with b
Underlying<string> u; // Extension<string> is a compatible extension with u
Underlying<int> u2; // But no substitution of Extension<T> is compatible with u2
Underlying<string?> u3; // Extensions<string?> is a compatible extension with u3
// but its usage will produce a warningNote: members from some other implicit extension type can apply to an extension type:
explicit extension E1 for C
{
void M()
{
this.M2(); // ok, E2 is compatible with type E1 since C is a base type of E1 and E2 extends C
}
}
implicit extension E2 for C
{
public void M2() { }
}Note: there may be multiple substitutions for a given compatible extension type:
_ = C.M; // ambiguous, C<int>.M and C<string>.M are both applicable
interface I<T> { }
class C : I<int>, I<string> { }
implicit extension E<T> for I<T>
{
public static string M = null;
}TL;DR: Given an underlying type, we'll search enclosing types and namespaces
(and their imports) for compatible extensions and for each "layer" we'll do member lookups.
Given an extension type, we'll do an extension member lookup for its extended type.
If the member_access occurs as the primary_expression of an invocation_expression, the member is said to be invoked.
Given a member_access of the form E.I and T the type of E, the objective
is to find an extension member X.I or an extension method group X.I, if possible.
We process as follows:
- We find
Uas the underlying type ofT. IfTis not an extension type, thenUisT. - Starting with the closest enclosing type declaration, continuing with each enclosing type declaration,
then continuing with each enclosing namespace declaration, and ending with
the containing compilation unit, successive attempts are made to find a candidate set of extension members:
- If the given type, namespace or compilation unit directly contains extension types, those will be considered first.
- If namespaces imported by using-namespace directives in the given namespace or compilation unit directly contain extension types, those will be considered second.
- Build a set of extension methods and extension types members:
- If
Eis a value (not a type) and if the scope contains eligible extension methods, then merge this set into the result of the lookup. TODO4 this doesn't fit, as eligibility is based on applicability which requires arguments - Look for extension type members:
- Check which extension types are compatible with the given underlying type
Uand collect resulting compatible substituted extension types. - Perform member lookup for
Iin each compatible substituted extension typeX(note this takes into account whether the member is invoked). - Merge the results
- Check which extension types are compatible with the given underlying type
- Next, members that are hidden by other members are removed from the set.
(note: "base types" means "underlying type" for extension types) - Next, less specific extension members are removed if they are "hidden" by more specific extension members.
For every memberX.Min the set, whereXis the extension type in which the memberMis declared, if no member in the set has a containing typeYthat is more specific thanXthen the following rules are applied:- If
Mis a constant, field, property, event, or enumeration member, then all members declared in a less specific type thanXare removed from the set. - If
Mis a type declaration, then all non-types declared in a less specific type thanXare removed from the set, and all type declarations with the same number of type parameters asMdeclared in a less specific type thanXare removed from the set. - If
Mis a method, then all non-method members declared in a less specific type thanXare removed from the set.
- If
- If
- Finally, having removed hidden members, the result of the lookup is determined:
- If the set is empty, proceed to the next enclosing scope.
- If the set consists of a single member that is not a method, then this member is the result of the lookup.
- Otherwise, if the set contains only methods, then this method group is the result of the lookup.
- Otherwise, the lookup is ambiguous, and a binding-time error occurs.
- Otherwise, continue the search through namespaces and their imports.
- If no candidate set is found in any enclosing namespace declaration or compilation unit, the result of the lookup is empty.
The preceding rules mean that:
- extension members available in inner type declarations take precedence over extension members available in outer type declarations,
- extension members available in inner namespace declarations take precedence over extension members available in outer namespace declarations,
- and that extension members declared directly in a namespace take precedence over extension members imported into that same namespace with a using namespace directive.
The difference between invocation and non-invocation handling is that for invocation scenarios, we can look past a result and continue looking at enclosing namespaces.
For example, if an "inner" extension has a method void M(int) and an "outer" extension
has a method void M(string), an extension member lookup for M("hello") will look over
the int extension and successfully find the string extension.
On the other hand, if an "inner" extension has an int property and an "outer" extension
has a string property, an assignment of a string to that property will fail, as
extension member lookup will find the int property and stop there.
For context see extension method invocation rules.
TL;DR: As part of extension member lookup, extension invocations and overload resolution, we consider that members from "less specific" extension types are "hidden" by members from "more specific" extension types.
If X extends C and Y extends D, X is considered less specific than Y
when:
Cis a base type ofD, orCis an interface implemented byD.
For example:
C.M(42); // extension method E2.M is preferred as E1.M is less specific
class Base { }
class C : Base { }
implicit extension E1 for Base
{
public static int M(int i) => throw null;
}
implicit extension E2 for C
{
public static int M(int i) => i;
}_ = C.P; // extension property E2.P is preferred as E1.P is less specific
class Base { }
class C : Base { }
implicit extension E1 for Base
{
public static int P => throw null;
}
implicit extension E2 for C
{
public static int P => i;
}We update the overload resolution section as follows:
Each of these contexts defines the set of candidate function members and the list of arguments in its own unique way. For instance, the set of candidates for a method invocation does not include methods marked override, methods in a base class are not candidates if any method in a derived class is applicable, *and extension methods in a less specific extension type are not candidates if any extension method in a more specific extension type is applicable.
The rules for determining the natural function type of a method group are modified as follows:
- For each scope, we construct the set of all candidate methods:
- for the initial scope, methods on the relevant type with arity matching the provided type arguments and satisfying constraints with the provided type arguments are in the set if they are static and the receiver is a type, or if they are non-static and the receiver is a value
- extension methods in that scope that can be substituted with the provided type arguments and reduced using the value of the receiver while satisfying constraints are in the set
- *methods from compatible implicit extension types applicable in that scope which can be substituted with the provided type arguments and satisfying constraints with those are in the set
- If we have no candidates in the given scope, proceed to the next scope.
- If the signatures of all the candidates do not match, then the method group doesn't have a natural type
- Otherwise, resulting signature is used as the natural type
- If the scopes are exhausted, then the method group doesn't have a natural type
Note: extension types members and extension methods are considered on par, as illustrated by this example:
var x = new C().M; // no natural function type
class C { }
implicit extension E for C
{
public static void M() { }
}
static class Extensions
{
public static void M(this C c, int i) { }
}
For context see Identical simple names and type names. TODO3
No changes to method group conversion rules.
The extension behavior falls out of method invocation rules.
The current simple assignment rules state:
When a property or indexer declared in a struct_type is the target of an assignment, the instance expression associated with the property or indexer access shall be classified as a variable. If the instance expression is classified as a value, a binding-time error occurs.
We're expecting those rules to be updated to check whether the receiver is a reference or value type instead. Then the rule will also apply to extension properties/indexers/events.
TODO update this section once the rules are spelled out for dotnet/csharpstandard#1078
1.Property = 42; // Error reporting that 1 (which is a struct) is not a variable
implicit extension E for int
{
public int Property { set => throw null; }
}
Extensions are emitted as static class types with an extension marker method.
The extension marker method encodes the underlying type as parameter.
It is public and static, and is called <ImplicitExtension>$ for implicit extensions and
<ExplicitExtension>$ for explicit extensions.
It allows roundtripping of extension symbols through metadata (full and reference assemblies).
For example: implicit extension R for UnderlyingType yields
public static void <ImplicitExtension>$(UnderlyingType).
Instance method/property/indexer declarations in source are represented as static declarations in metadata:
- A new parameter is added at the beginning, it represents
thiswith erased extension type. - The parameter's name is unspeakable
- The type of the parameter is the extended type
- A
modreq(System.Runtime.CompilerServices.ExtensionAttribute)is added to the type. On import the presence of the modreq is checked by verifying fully qualified name of the ExtensionAttribute type. Location of the type and its other properties are not checked. - The parameter is a 'ref' parameter, unless its type is known to be a reference type
A method example:
public implicit extension E for C
{
public void Method()
{
}
}
public class C {} .method public hidebysig static
void Method (
class C modreq([System.Runtime]System.Runtime.CompilerServices.ExtensionAttribute) '<>4__this'
) cil managed
Other tools and compilers that follow a requirement to not consume APIs with an unknown modreq will be unable to consume the method. Including VB compiler and previous C# compiler.
A property example:
public implicit extension E for C
{
public int P1
{
get => ...;
set => ...;
}
}
public struct C
{
} .method public hidebysig specialname static
int32 get_P1 (
valuetype C modreq([System.Runtime]System.Runtime.CompilerServices.ExtensionAttribute)& '<>4__this'
) cil managed
.method public hidebysig specialname static
void set_P1 (
valuetype C modreq([System.Runtime]System.Runtime.CompilerServices.ExtensionAttribute)& '<>4__this',
int32 'value'
) cil managed
.property int32 P1(
valuetype C modreq([System.Runtime]System.Runtime.CompilerServices.ExtensionAttribute)& '<>4__this'
)
{
.get int32 E::get_P1(valuetype C modreq([System.Runtime]System.Runtime.CompilerServices.ExtensionAttribute)&)
.set void E::set_P1(valuetype C modreq([System.Runtime]System.Runtime.CompilerServices.ExtensionAttribute)&, int32)
}
Other tools and compilers that follow a requirement to not consume APIs with an unknown modreq will be unable to consume the property/indexer and the accessors. Including VB compiler and previous C# compiler.
An event example:
public implicit extension E for C
{
public event System.Action E1
{
add => ...;
remove => ...;
}
}
public class C
{
} .method public hidebysig specialname static
void add_E1 (
class C modreq([System.Runtime]System.Runtime.CompilerServices.ExtensionAttribute) '<>4__this',
class [System.Runtime]System.Action 'value'
) cil managed
.method public hidebysig specialname static
void remove_E1 (
class C modreq([System.Runtime]System.Runtime.CompilerServices.ExtensionAttribute) '<>4__this',
class [System.Runtime]System.Action 'value'
) cil managed
.event [System.Runtime]System.Action E1
{
.addon void E::add_E1(class C modreq([System.Runtime]System.Runtime.CompilerServices.ExtensionAttribute), class [System.Runtime]System.Action)
.removeon void E::remove_E1(class C modreq([System.Runtime]System.Runtime.CompilerServices.ExtensionAttribute), class [System.Runtime]System.Action)
}
Note, the extra parameter for event accessors are not CLS compliant, therefore, tools and other compilers likely won't be able to consume them as regular static events. Other tools and compilers that follow a requirement to not consume APIs with an unknown modreq will be unable to consume the accessors. Including VB compiler and previous C# compiler.
Since at the moment we are not supporting overriding or interface implementation by extension types, presence of the
modreq(System.Runtime.CompilerServices.ExtensionAttribute) is sufficient to avoid a signature conflict with a user
defined static member because, given the restriction, there is no way for any modreq to get its way into
signature of a user defined static method. Support for interface implementation is likely to change that and a signature
conflict could possibly occur in some edge cases. Therefore, we should block consumption/implementation of interface methods
with ExtensionAttribute modreq.
However, other tools and compilers (VB, for example) won't be able to disambiguate APIs based on presence of the modreq.
public implicit extension E for C
{
public void Method()
{
System.Console.Write(1);
}
public static void Method(C c)
{
System.Console.Write(2);
}
}
public class C
{
}C# consumer:
class Program
{
static void Main()
{
var c = new C();
c.Method(); // Prints 1
C.Method(c); // Prints 2
}
}VB consumer:
Class Program
Shared Sub Main()
Dim c = new C()
E.Method(c) ' BC31429: 'Method' is ambiguous because multiple kinds of members with this name exist in structure 'E'.
End Sub
End ClassWhen a reference is provided for the special ref <>4__this parameter and the type of the parameter could be a reference
type at runtime, compiler should ensure that a reference to a temporary location with a copy of a value from
the user specified location is provided instead of the original user specified location. This redirection should happen only when the type
is a reference type during execution of the code. The goal is to ensure that the instance on which the extension method is executed
doesn't change for the duration of the method call.
Example:
class C
{
}
class Program
{
C _f;
void Test() => _f.Method();
}
public implicit extension E<T> for T
{
public void Method() {}
}The emitted body of Program.Test will be something like:
C temp = _f;
E<C>.Method(ref temp);instead of
E<C>.Method(ref _f);which would allow to change the target instance by changing value stored in _f while Method is executed.
A field_declaration in a extension_declaration shall explicitly include a static modifier.
Auto-properties must still be static (since instance fields are disallowed).