TODO3 No duplicate base extensions (to avoid ambiguities)
TODO3 issue with variance of extended type if we erase to a ref struct with a ref field.
TODO2 need to spec why extension properties are not found during lookup for attribute properties, or explicitly disallow them
TODO2 adjust scoping rules so that type parameters are in scope within the 'for'
TODO2 check Method type inference: 7.5.2.9 Lower-bound interfaces
TODO2 extensions are disallowed within interfaces with variant type parameters
TODO2 We should likely allow constructors and required
properties
TODO attributes and attribute targets
TODO4 revise 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) => ...
}
TODO4 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 LDM
TODO4 We want to prefer an extension on a derived type over an extension on a base type, but not sure how to specify that yet. For example:
C.M(42); // Should prefer extension method E2.M, as M(C, int) is a better function than M(Base, int)
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; // Should prefer E2.P
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;
}
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 ReadOnlySpan<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 for "extension interfaces".
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.
This proposal is divided into three parts, all relating to extending existing types:
A. static implicit extensions
B. implicit and explicit extensions with members
C. implicit and explixit extensions that implement interfaces
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)? extension_base_type_list? type_parameter_constraints_clause* extension_body
;
extension_base_type_list
: ':' extension_or_interface_type_list
;
extension_or_interface_type_list
: interface_type_list
: extension_type (',' extension_or_interface_type_list)
;
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 example with multiple base explicit extension:
explicit extension DiamondExtension for NarrowerUnderlyingType : BaseExtension1, Interface1, BaseExtension2, Interface2 { }`
TODO should we have a naming convention like Extension
suffixes? (DataObjectExtension
)
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, and between an extension and its base extensions.
An extension type satisfies the constraints satisfied by its underlying type (see section on constraints). In phase C, some additional constraints can be satisfied (additional implemented interfaces).
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 Phase C).
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.
TODO2 we need rules to only allow an underlying type that is compatible with the base extensions.
TODO should the underlying type be inferred when none was specified but base extensions are specified?
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).
We'll use "inherits" for relationship to inherited/base extensions
(comparable to "implements" for relationship to implemented interfaces).
struct U { }
explicit extension X for U { }
explicit extension Y for U : X, X1 { }
"Y has underlying type U"
"Y extends U"
"Y inherits X and X1"
"Derived extension Y inherits members from inherited/base extensions X and X1"
Similarly, extension don't have a base type, but have base extensions.
implicit extension R<T> for T where T : I1, I2 { }
implicit extension R<T> for T where T : INumber<T> { }
An extension may be a value or reference type, and this may not be known at compile-time.
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.
- *The base extensions 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 (TODO not yet specified).
We modify the protected access rules as follows:
*When a protected
(or other accessibility with protected
) extension member is accessed,
the access shall take place within an extension declaration that derives from
the extension in which it is declared.
Furthermore, the access is required to take place through an instance of that
derived extension type or an extension type constructed from it.
This restriction prevents one derived extension from accessing protected members of
other derived extensions, even when the members are inherited from the same base extension.
Note: the rules still disallow access to protected members of the underlying type through the extension type.
TODO
Let B
be a base class that declares a protected instance member M
,
and let D
be a class that derives from B
. Within the class_body of D
,
access to M
can take one of the following forms:
- An unqualified type_name or primary_expression of the form
M
. - A primary_expression of the form
E.M
, provided the type ofE
isT
or a class derived fromT
, whereT
is the classD
, or a class type constructed fromD
. - A primary_expression of the form
base.M
. - A primary_expression of the form
base[
argument_list]
.
class Base
{
protected void M() { }
}
extension E1 for Base
{
// cannot use Base.M
protected void M2() { }
}
extension E2 for Base : E1
{
// can use E1.M2
}
The extension type members may not use the virtual
, abstract
, sealed
, override
modifiers.
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, or an extension vs.
one of its base extensions are considered to be the same signature.
TODO2 this needs to be refined to allow overload on different underlying types.
explicit extension ObjectExtension : object;
explicit extension StringExtension : string, ObjectExtension;
void M(ObjectExtension r)
void M(StringExtension r) // overload is okay
Shadowing includes underlying type and inherited extensions.
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
class U { }
explicit extension R for U { public void M() { } }
explicit extension R2 for U : R { /*new*/ public void M() { } } // wins when dealing with an R2
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.
TODO allow this
(of type current extension).
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.
Extension methods are disallowed.
TODO allow this
(of type current extension).
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.
TODO any special rules?
extension Extension : UnderlyingType
{
class NestedType { }
}
class UnderlyingType { }
UnderlyingType.NestedType x = null; // okay
TODO TODO2 Event with an associated instance field (error)
TODO
TODO
TODO
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.
TODO
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
C
represent 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 typeA
is convertible to typeC
by one of the following:- An identity conversion
- An implicit reference conversion
- A boxing conversion, provided that type
A
is a non-nullable value type. - An implicit reference, boxing or type parameter conversion from a type parameter
A
toC
.
- If the constraint is the reference type constraint (
class
), the typeA
shall satisfy one of the following:A
is an interface type, class type, delegate type, array type or the dynamic type.A
is a type parameter that is known to be a reference type.- *
A
is an extension type with an underlying type that satisfies the reference type constraint.
- If the constraint is the value type constraint (
struct
), the typeA
shall satisfy one of the following:A
is astruct
type orenum
type, but not a nullable value type.A
is a type parameter having the value type constraint.- *
A
is an extension type with an underlying type that satisfies the value type constraint.
- If the constraint is the constructor constraint
new()
, the typeA
shall not beabstract
and shall have a public parameterless constructor. This is satisfied if one of the following is true:A
is a value type, since all value types have a public default constructor.A
is a type parameter having the constructor constraint.A
is a type parameter having the value type constraint.A
is aclass
that is not abstract and contains an explicitly declared public constructor with no parameters.A
is notabstract
and has a default constructor.- *
A
is 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?
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.
TODO2 Open question on top-level nullability on underlying type.
TODO2 disallow nullable annotation on base extension? Or at least need to clarify what Extension?
means in various scenarios.
TODO2 types may not be called "extension" (reserved, break)
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 result is that namespace.
-
... the result is that type constructed with the given type arguments.
-
If
E
is classified as a type, ifE
is not a type parameter, and if a member lookup ofI
inE
withK
type parameters produces a match, thenE.I
is evaluated and classified as follows:Note: When the result of such a member lookup is a method group and
K
is zero, the method group can contain methods having type parameters. This allows such methods to be considered for type argument inferencing. end note- If
I
identifies a type, then the result is that type constructed with any given type arguments. - If
I
identifies one or more methods, then the result is a method group with no associated instance expression. - If
I
identifies a static property, then the result is a property access with no associated instance expression. - If
I
identifies 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
I
inE
. - Otherwise, the result is a variable, namely the static field
I
inE
.
- 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
I
identifies 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.I
is processed exactly as ifI
were 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
I
identifies a constant, then the result is a value, namely the value of that constant. - If
I
identifies an enumeration member, then the result is a value, namely the value of that enumeration member. - *
Otherwise,E.I
is an invalid member reference, and a compile-time error occurs.
- If
-
*If
E.I
is not invoked andE
is classified as a type, ifE
is not a type parameter, and if an extension member lookup ofI
inE
withK
type parameters produces a match, thenE.I
is evaluated and classified as follows:
... -
If
E
is a property access, indexer access, variable, or value, the type of which isT
, and a member lookup ofI
inT
withK
type arguments produces a match, thenE.I
is evaluated and classified as follows:
... -
*(only relevant in phase B) If
E.I
is not invoked andE
is a property access, indexer access, variable, or value, the type of which isT
, whereT
is not a type parameter, and an extension member lookup ofI
inT
withK
type arguments produces a match, thenE.I
is evaluated and classified as follows:
... -
Otherwise, an attempt is made to process
E.I
as an *extension invocation. If this fails,E.I
is 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. We can also get to extension invocation in:
- invocation scenarios where the set of applicable candidate methods is empty.
- indexer access scenarios where the set of applicable candidate indexers is empty.
- TODO there may be more scenarios (operator resolution, delegate conversion, natural function types)
That is covered below.
TODO3 Is the "where T
is not a type parameter" portion still relevant?
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. [...]
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
Type
case, an substituted compatible implicit extension typeX
forType
so that the corresponding invocation can take place:
X . «identifier» ( )
X . «identifier» ( «args» )
X . «identifier» < «typeargs» > ( )
X . «identifier» < «typeargs» > ( «args» )
- for the
expr
case where the expression has typeType
, a substituted compatible implicit extension typeX
forType
so 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
expr
case, the best type_nameC
so 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.
TODO4 need to merge extension members and extension methods
-
First, try extension types:
- Check which extension types in the current scope are compatible with the given underlying type
Type
and collect resulting compatible substituted extension types. - Perform member lookup for
identifier
in 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 "base extensions and underlying type" for extension types) - Finally, having removed hidden 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
expr
case):- 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.
TODO clarify behavior for extension on object
or dynamic
used as dynamic.M()
?
TL;DR: If no candidate is applicable, then we attempt extension indexer access instead.
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, or interface 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:
/[...]
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, or interface type T
, and A
is an argument_list, consists of the following steps:
- The set of indexers provided by
T
is constructed. [...] - The set is reduced to those indexers that are applicable and not hidden by other indexers. [...]
- *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.
- /[...]
/[...]
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,
Type
is the underlying type of that extension type. Otherwise,Type
is 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
Type
and 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 "base extensions and 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.
TL;DR: Member lookup on an extension type includes members from its base extensions, 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
T
isobject
ordynamic
, thenT
has no base type. - If
T
is an enum_type, the base types ofT
are the class typesSystem.Enum
,System.ValueType
, andobject
. - If
T
is a struct_type, the base types ofT
are the class typesSystem.ValueType
andobject
. - If
T
is a class_type, the base types ofT
are the base classes ofT
, including the class typeobject
. - If
T
is an interface_type, the base types ofT
are the base interfaces ofT
and the class typeobject
. - If
T
is an array_type, the base types ofT
are the class typesSystem.Array
andobject
. - If
T
is a delegate_type, the base types ofT
are the class typesSystem.Delegate
andobject
. - *If
T
is an extension_type, the base types ofT
are the base extensions ofT
and the extended type ofT
and its base types.
TODO will need to revisit once we have inheritance and we allow variance of extended 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 an extension type we can use (including required substitutions).
An extension type X
is compatible with given type U
if:
X
is non-generic and its underlying type isU
, a base type ofU
or an implemented interface ofU
- a possible type substitution on the type parameters of
X
yields underlying typeU
, a base type ofU
or an implemented interface ofU
.
Such substitution is unique (because of the requirement that all type parameters from the extension appear in the underlying type).
We call the resulting substituted typeX
the "compatible substituted extension type".
#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 warning
Note: 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() { }
}
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.
TODO4 confirm this with WG and LDM
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
U
as the underlying type ofT
. IfT
is not an extension type, thenU
isT
. - 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
E
is 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
U
and collect resulting compatible substituted extension types. - Perform member lookup for
I
in each compatible substituted extension typeX
(note this takes into account whether the member is invoked). - Merge the results TODO4 spec how we deal with duplicate or near duplicate entries (for example, field E1.Member and method E2.Member)
- 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 "base extensions and underlying type" for extension types)
- 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 and the member is invoked,
overload resolution is applied to the candidate set. TODO3 we can never get here...
- If a single best method is found, this member is the result of the lookup.
- If no best method is found, continue the search.
- Otherwise (ambiguity), a compile-time error occurs.
- 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.
TODO3 explain static usings:
meaning of using static SomeType;
(probably should look for extension types declared within SomeType
)
meaning of using static Extension;
using static ClassicExtensionType;
using static NewExtensionType; // What should this do?
// Are the static methods from that extension in scope?
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.
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) { }
}
TODO4 should we disambiguate when signatures match?
var x = new C().M; // ambiguous
class C { }
implicit extension E for C
{
public static void M() { }
}
static class Extensions
{
public static void M(this C c) { }
}
TODO4 would like to brainstorm tweaks to member access and natural function type to make this work better:
Note: by the current rules, there are some unfortunate interactions between member access and natural function type.
Note: When the result of such a member lookup is a method group and K is zero, the method group can contain methods having type parameters. This allows such methods to be considered for type argument inferencing. end note
var x = C.Member; // error: member lookup finds C.Member (method group) and lacks type arguments to apply to that match
class C
{
public static void Member<T>() { }
}
implicit extension E for C
{
public static int Member = 42;
}
For context see Identical simple names and type names. TODO3
TODO review with LDM
We'll start by disallowing base access
within extension types.
Casting seems an adequate solution to access hidden members: ((R)r2).M()
.
TODO Maybe base.
could refer to underlying value.
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.F
in the set, whereC
is the type in which the methodF
is declared, all methods declared in a base type ofC
are 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
}
TODO3 write this section
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 indexer access section 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
T
is constructed. The set consists of all indexers declared inT
or a base type ofT
that are not override declarations and are accessible in the current context (§7.5). - The set is reduced to those indexers that are applicable and not hidden by other indexers. The following rules are applied to each indexer
S.I
in the set, whereS
is the type in which the indexerI
is declared:- If
I
is not applicable with respect toA
, thenI
is removed from the set. - If
I
is applicable with respect toA
, then all indexers declared in a base type ofS
are removed from the set. - If
I
is applicable with respect toA
andS
is a class type other thanobject
, all indexers declared in an interface are removed from the set.
- If
- 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.
- The index expressions of the argument_list are evaluated in order, from left to right. The result of processing the indexer access is an expression classified as an indexer access. The indexer access expression references the indexer determined in the step above, and has an associated instance expression of
P
and an associated argument list ofA
, and an associated type that is the type of the indexer. IfT
is a class type, the associated type is picked from the first declaration or override of the indexer found when starting withT
and searching through its base classes.
TODO User-defined conversion should be allowed, except where it conflicts with a built-in conversion (such as with an underlying type).
TODO
https://github.com/dotnet/csharpstandard/blob/draft-v7/standard/expressions.md#128164-collection-initializers
Explain how extension types factor in when resolving Add
calls.
https://github.com/dotnet/csharpstandard/blob/draft-v7/standard/conversions.md#108-method-group-conversions
TODO A single method is selected corresponding to a method invocation,
but with some tweaks related to normal form and optional parameters.
TODO There's also the scenario where a method group contains a single method (lambda improvements).
TODO There's also the scenario where method groups have a natural type even in the presence
of multiple extension methods, and even though the scenario remains an error for other reasons.
A method group has a natural type if all candidate methods in the method group have a common signature. (If the method group may include extension methods, the candidates include the containing type and all extension method scopes.)
TODO 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 (for example Current
property in foreach
).
TODO3 revise this to use a regular struct
struct Extension { UnderlyingType underlyingValue; } // Avoid copying via Unsafe.As
Extensions are implemented as ref structs with an extension marker method.
The type is marked with Obsolete and CompilerFeatureRequired attributes.
The extension marker method encodes the underlying type and base extensions as parameters in that order.
The marker method is called <ImplicitExtension>$
for implicit extensions and
<ExplicitExtension>$
for explicit extensions.
For example: implicit extension R for UnderlyingType : BaseExtension1, BaseExtension2
yields
private static void <ImplicitExtension>$(UnderlyingType, BaseExtension1, BaseExtension2)
.
If the extension has any instance member, then we emit a ref field (of underlying type)
into the ref struct and a constructor.
TODO2 The wrapping can be done with a static unspeakable factory method
Values of extension types are left as values of the underlying value type, until an extension member is accessed. When an extension member is accessed, an extension instance is created with a reference to the underlying value and the member is accessed on that instance.
Extension r = default(UnderlyingType); // emitted as a local of type `UnderlyingType`
r.ExtensionMember(); // emitted as `new Extension(ref r).ExtensionMember();`
Extensions appearing in signatures are emitted as the extension's underlying type marked with a modopt of the extension type.
void M(Extension r) // emitted as `void M(modopt(Extension) UnderlyingType r)`
TODO issues in async code with ref structs
In this first subset of the feature, the syntax is restricted to implicit extension_declaration
and containing only constant_declaration members and static field_declaration,
method_declaration, property_declaration and type_declaration members.
TODO: events?
In this second subset of the feature, the explicit extension_declaration becomes allowed and non-static members other than fields or auto-properties become allowed.
The restrictions on modifiers from phase A remain (new
).
Non-static members become allowed in phase B.
A field_declaration in a extension_declaration shall explicitly include a static
modifier.
TODO allow this
(of type current extension).
Auto-properties must still be static (since instance fields are disallowed).
TODO allow this
(of type current extension).
TODO
explicit extension R : 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.
TODO2: Could we make the conversion to be explicit identity conversion instead
of implicit?
TODO
TODO
TODO