Description
Extension interface implementation
- Specification:
- Discussion: [Proposal]: Extension interface implementation #9320
Summary
An implementable model for extension interface implementation has not yet been proposed. Previous proposals were also intimately tied to roles/shapes. This proposal includes an ECMA-335 design that allows extension interface implementation at the CLR level, as well as a proposal for integration into C#.
Currently, the set of interfaces implemented by a type is controlled by the type definition (and IDynamicInterfaceCastable
, which will not be covered in this proposal). All interfaces implemented by a type must appear in that type's definition. This contains two significant weaknesses:
- It prevents adding interfaces to external types, particularly types that appear in the core libraries. It is often desirable for libraries (like serializers, loggers, eventing libraries, etc) to provide new interfaces that represent domain-specific object models, and then implement those object models on the core types of .NET (primitive types, arrays, lists, etc). Because interface implementations must always occur on type definitions and users do not control the definition of primitive types, this makes these augmentations impossible.
- It does not support "conditional implementation." Some interfaces require all members to implement the interface before the containing type can implement it. For example, deep equality may require all members to implement a deep equality interface. This is problematic for generic types because there is no way to provide a "conditional" constraint. Using the same example, a
List<T>
should only be deep equal ifT
implements deep equal. However,List<T>
should not requireT
to be deep equal in all cases.
The proposal is to relax this restriction and allow interface implementations to appear outside of the type definition in an extension interface implementation. While extension interface implementations do not have to appear inside the definition of the implementing type, they do have restrictions of their own:
- They must appear in the same compilation unit as either the implementing type or the implemented interface
- Two implementations must not overlap. That is, there must be a single best implementation for any given type-interface pair.
The preceding rules guarantee that,
- There is either one unique implementation or no implementation for any given type-interface pair.
- This implementation (or its nonexistence) can be determined by examining only the compilation unit containing the type definition and the compilation unit containing the implemented interface.
- Checking for interface implementation is idempotent. After an implementation for a given type-interface pair is found (or not found), no operations, including running or loading new code, can change the result.
C#
Extension interfaces are declared similar to other extensions:
interface IPrint
{
void Print();
}
static class IntExt
{
extension(int) : IPrint
{
void IPrint.Print() { Console.WriteLine(this); }
}
extension<T>(List<T>) : IPrint
where T : IPrint
{
void IPrint.Print()
{
foreach (var x in this)
{
x.Print();
}
}
}
}
The compiler would be responsible for determining that no two implementations overlap. More precisely, an extension implementation with extended type T1
and interface I1
conflicts with any implementation (direct or extension) with type T2
and interface I2
if T1
is reference-convertible to T2
and I1
is reference-convertible to I2
.
- To do: The above definition is almost certainly not sufficient to capture all possible overlapping implementations.
Motivating example
It seems like the restrictions around foreign types are very strict, but they still allow useful patterns.
Consider "deep" equality. In .NET, collections implement shallow equality, meaning that two collections are considered equivalent if and only if they are reference-equal. Sometimes this is OK, but other times it would be very useful to compare collections element-by-element instead. For example, in unit testing it would be nice if Assert.Equal
could use deep equality instead of shallow equality.
Unfortunately, deep equality is almost impossible to implement in a general manner in .NET. The problem is that deep equality fundamentally requires adding an equality implementation conditionally for an entire type heirarchy. With extension interface implementations, things could get much easier. A unit testing library might define
interface IDeepEqual<T>
{
bool Equal(T other);
}
to represent deep equality, analogous to the existing IEquatable<T>
interface. Then, they could define a method to use the interface,
bool Assert.Equal<T>(T expected, T actual) where T : IDeepEqual<T>
{
if (!expected.Equal(actual))
{
throw ...
}
}
Normally this method would be very hard to use because it wouldn't work for things like int
or string
. You could define overloads, but then there's List<string>
and Dictionary<string, string>
. The set of required overloads is infinite.
By using extension interface implementation, we can use the compiler/runtime to effectively implement the interface, on demand. Because the unit testing library defines the IDeepEqual<T>
interface itself, it can define extensions on other types. It would extend primitive types directly:
static class PrimExt
{
extension(string) : IDeepEqual<string>
{
public bool Equal(string other) => this == other;
}
extension(int) : IDeepEqual<int>
{
public bool Equal(int other) => this == other;
}
}
It can extend collection types:
static class CollectExt
{
extension<T>(T[]) : IDeepEqual<T[]> where T : IDeepEqual
{
public bool Equal(T[] other)
{
if (this.Length != other.Length) return false;
for (int i = 0; i < this.Length; i++)
{
if (!this[i].Equal(other[i])) return false;
}
return true;
}
}
}
Note that even if you controlled the definition of array, you could not write the above today, because you cannot conditionally implement an interface depending on whether a constraint is satisfied.
You could even provide an implementation for all types that implement an interface:
static class IListExt
{
extension<T, TList>(TList) : IDeepEqual<TList>
where T : IDeepEqual<T>
where TList : IList<T>
{
public bool Equal(TList other)
{
if (this.Length != other.Length) return false;
for (int i = 0; i < this.Length; i++)
{
if (!this[i].Equal(other[i])) return false;
}
return true;
}
}
}
In each case the runtime/compiler will "wire up" the interface implementations so that each type in the chain will recursively query for an available implementation until all requests are satisfied.
The main downside of the foreign type restriction seems to be that libraries might have to define their own interfaces to take advantage. This could lead to many libraries have duplicate definitions. Overall, this doesn't seem very bad. A few duplicate interfaces does not seem like a significant practical problem. In the case that a particular definition is truly so pervasive, it seems likely that the community can settle on a single library to provide that abstraction, or even get the interface added to the core framework.
ECMA-335
ExtInterfaceImpl
Explicit interface implementations are currently recorded in the InterfaceImpl
table as a map from entries in the TypeDef table to interface (via TypeSpec). Extension interface implementations will be recorded in a new table, ExtInterfaceImpl
.
The ExtInterfaceImpl
table has the following columns:
- Type, an entry into the
TypeSpec
table - Interface, an entry into the
TypeSpec
table - Impl, a coded
TypeDefOrRef
index
A single row represents attaching an interface implementation (Impl) for an interface (Interface) to a type (Type).
An interface implementation here is represented as a reference to a custom implementing class. For each abstract member in Interface, Impl must contain a corresponding MethodImpl entry which matches the signature with one exception: to allow specifying the correct receiver type, all instance abstract members in Interface will be represented by a static member in Impl with the first parameter's type matching the entry in the _Type_
column.
For example, the above IPrint
interface could be implemented on the int
type through an implementing class __Impl
as follows:
static class __Impl
{
public static void IPrint.Print(int @this) { ... }
}
Type and Interface entries can also make use of generic parameters. The definitions of generic parameters will come from the GenericParam
entries attached to Impl. For instance, if the TypeSpec
inside a Type entry pointed to the signature blob List<!0>
, !0
would refer to the first generic parameter in Impl.
For example, an extension implementation of an IEnumerable<T>
interface on a MyList<T>
type when T
is a reference type (where T : class
) would include:
- An Impl type
class __Impl<T> where T : class
that contains the appropriate implementation methods - An entry in
ExtInterfaceImpl
as follows where Type isList<!0>
, Interface isIEnumerable<!0>
, and Impl is the TypeDefOrRef__Impl<T>
Interface implementation rules
There will be a new rule for interface implementation logic that takes advantage of the ExtInterfaceImpl
table. When testing if a given type T
implements an interface I
, the existing checks for direct implementation on the type T
will be followed. Only if the check fails will the new rules take effect (i.e., existing successful resolution is unaffected). Otherwise, each row R
in ExtInterfaceImpl
is scanned. The given row R
is considered to provide a successful implementation if:
- The interface entry Interface in
R
is compatible withI
, and - The type entry Type in
R
is compatible withT
For any given x
and y
, x
is compatible with y
if:
x
contains no type parameters andx
is convertible toy
, or- The open type of
x
and the open type ofy
are identical and, if all type parameters t_i inx
were replaced with the corresponding type argument t_i' iny
, all constraints of t_i would be satisfied by t_i'.
Design meetings
Metadata
Metadata
Assignees
Labels
Type
Projects
Status