-
Notifications
You must be signed in to change notification settings - Fork 1k
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
The design space for extensions #8548
Comments
Just to reiterate the Scala 3 approach, which I think does solve for a lot of these problems (although not necessarily all of them). The extensions are member-based, so it's completely unopinionated regarding the container class into which they are declared. However, it also allows grouping the members by target so you don't have to constantly repeat the target for each member. The extension itself can be declared as generic, allowing for generic targets and constraints, and that generic type parameter is flattened into the member signatures as the first generic type parameter. The extension uses primary constructor syntax, so the target declaration is very flexible as to the parameter name and type, and supports annotations like any normal parameter would. Scala: // can be declared in any class
object Enumerable {
// single member, non-generic target, generic method
extension (source: Seq[_]) def ofType[T](): Seq[T] = ???
// multiple members, generic target
extension[T] (source: Seq[T]) {
// non generic extension method
def filter(predicate: T => Boolean): Seq[T] = ???
// generic extension method
def map[TR](mapper: T => TR): Seq[TR] = ???
// extension property
def isEmpty: Boolean = ???
// extension operator
def :: (other: Seq[T]): Seq[T] = ???
}
// more complicated generics
extension[T, U] (source: Seq[T]) {
// generic extension operator
def + (other: Seq[U]): Seq[T] = ???
}
// can also include normal members
def Empty[T](): Seq[T] = ???
} I think the one use case it doesn't cover is when you want a generic target but the generic type parameter is not the first generic type parameter. Actually, nevermind, it does: // reordered generic type parameters
extension[TR, T] (source: Seq[T]) def map(mapper: T => TR): Seq[TR] = ??? I'm also not sure how that could translate to C# syntax. I guess this is somewhat close to the Anyway, food for thought. I think by borrowing from and combining existing language syntax elements like this it does help to simplify some of the more complicated cases without having to come up with a bunch of new syntax. |
Just some personal thoughts... Seeing how complex these considerations get… why do we even need a new kind of syntax for non-role extensions? I’ve wanted extensions properties once or twice, but really it’s not something that would allow entirely new things, or would it? What I’m really looking forward to is „extension types“ / „roles“ with their own type identities. I would use them soooo much. |
Ok, here's something I came up with. The idea is to take hybrid approach. Meet the new "for" clauseA "for" clause specifies the generic parameters which participate in binding of a member, the underlying type, the receiver mode and its modifiers. Receiver modesReceiver mode is an extension-only concept which determines how you will access the underlying type or the object for which your extension is called. I will describe 4 possible receiver modes, 2 for instance and 2 for static members. For instance members:
For static members:
public extension ExtensionA
{
// T goes into "for" because it is used in the underlying type
public void ExtMethod0(int someArg) for <T> List<T> list where T : class?
{
// "list" is a parameter now.
// You can access its members only by referring to them like 'list.Member'
// T is a type you get from binding when you call it like an instance method
}
public void ExtMethod1(int someArg) for <T> List<T> this where T : class?
{
// "this" is the receiver now.
// You can access its members by referring to them like 'this.Member' OR
// just 'Member' like if they were declared right here in the extension
// T is a type you get from binding when you call it like an instance method
}
// Generic parameters not used in the receiver type are specified in the method declaration.
// If generic parameters are present in both the "for" clause and the method declaration, their constraints are specified in a shared "where" clause.
public void ExtMethod2<U>(T someArg1, U someArg) for <T> List<T> this where T : class? where U : struct
{
// "this" is the receiver now.
// You can access its members by referring to them like 'this.Member' OR
// just 'Member' like if they were declared right here in the extension
// T is a type you get from binding when you call it like an instance method
// U is whatever specified at the call site
}
// This is how you include receiver modifiers in a "for" clause.
// It doesn't matter whether it's a named parameter or a this-receiver
public void ExtMethod3(T val) for <T> [SomeAttribute] ref T this where T : struct
{
// "this" is the receiver now. It is passed by-ref and has [SomeAttribute]
// You can access its members by referring to them like 'this.Member' OR
// just 'Member' like if they were declared right here in the extension
// T is a type you get from binding when you call it like an instance method
this = val;
}
// This is how you declare instance extension properties
// You always have to use 'this' as the receiver mode, since named parameter
// doesn't make sense here.
public int ExtProperty for <T> T this where T : class? => 1;
// And indexers (why they still can't named?)
// Although we have parameters here, let's not complicate further and only use 'this'.
public object this[int i] for <T> T this where T : class?
{
get ... set ...
}
// And static methods
// You don't have to specify 'default' receiver mode in this context.
public static T StaticExtMethod() for <T> T where T : class?
{
return ...
}
// self may be useful when you have a long underlying type which you don't want to repeat to access its members
public static T StaticExtMethod2() for <T> TypeA<TypeB<TypeC<T>>> self where T : class?
{
UnderlyingTypeStaticMethod1()
UnderlyingTypeStaticMethod2()
self.UnderlyingTypeStaticMethod3()
return ...
}
// And operators
public static explicit operator int(T obj) for <T> T where T : class?
{
return obj.GetHashCode();
}
// You got the idea
} How to call these?List<object> list = new();
int arg = 0;
// 0
list.ExtMethod0(arg);
// 1
list.ExtMethod1(arg);
// 2
list.ExtMethod2<object, int>(new object(), arg);
// Important: See Update 3 for more info.
list.ExtMethod2<.., int>(new object(), arg);
// 3
arg.ExtMethod3(1);
// Instance Property
_ = list.ExtProperty;
// Instance Indexer
_ = list[0];
// Static methods
_ = object.StaticExtMethod();
_ = TypeA<TypeB<TypeC<object>>>.StaticExtMethod2();
// Operator
_ = (int)(new object()); How to call these like static members on the extension type?When you call new extension methods like static ones, // 0
ExtensionA.ExtMethod1(list, arg);
// 1
ExtensionA.ExtMethod1(list, arg);
// 2
// Since this method requires additional parameter which cannot be inferred, you have to fill in generics explicitly.
ExtensionA.ExtMethod2<int, int>(list, new object(), arg);
// 3
ExtensionA.ExtMethod3(ref arg, 1);
// The other member kinds don't require compatibility with old extensions.
// They still can be lowered to static members of the extension type. Now, let's convert that extension to multiple type-based ones, which will be identical to previousThe "for" clause has moved to the extension type declaration ExtensionA.ExtMethod1(receiver, arg) vs ExtensionA<int>.ExtMethod1(receiver, arg) But we don't have generic extension types in C#? Well, in type-based approach we will, so this is required to have compatibility and to preserve extension syntax uniformity. public extension ExtensionA for <T> List<T> where T : class?
{
public void ExtMethod0(int someArg) for list
{
// "list" is a parameter now.
// You can access its members only by referring to them like 'list.Member'
// T is a type you get from binding when you call it like an instance method
}
public void ExtMethod1(int someArg) for this
{
// "this" is the receiver now.
// You can access its members by referring to them like 'this.Member' OR
// just 'Member' like if they were declared right here in the extension
// T is a type you get from binding when you call it like an instance method
}
public void ExtMethod2<U>(T someArg1, U someArg) for this
{
// "this" is the receiver now.
// You can access its members by referring to them like 'this.Member' OR
// just 'Member' like if they were declared right here in the extension
// T is a type you get from binding when you call it like an instance method
// U is whatever specified at the call site
}
}
public extension ExtensionB for <T> T where T : struct
{
public void ExtMethod3(T val) for [SomeAttribute] ref this
{
// "this" is the receiver now. It is passed by-ref and has [SomeAttribute]
// You can access its members by referring to them like 'this.Member' OR
// just 'Member' like if they were declared right here in the extension
// T is a type you get from binding when you call it like an instance method
this = val;
}
}
public extension ExtensionC for <T> T where T : class?
{
public int ExtProperty for this => 1;
public object this[int i] for this
{
get ... set ...
}
// 'default' is one of the static receiver modes and also literally means empty 'for' clause. This is in line with the 'default' generic constraint with similar meaning. It is just there to mark the method as an extension.
// Options for different naming may include '_', 'static', but personally I find 'default' the best.
// Also, since we already have a parent 'for' scope, there is an option to omit the 'for' clause here and make all static members in a 'for' scope extensions by default. I don't like this approach because it makes it impossible to declare non-extension statics if you use a type-based 'for' scope without extracting it to be a child scope like will be shown later.
// And finally an augmentation of the previous way: Another method modifier could be added which means "nonextension".
public static T StaticExtMethod() for default
{
return ...
}
public static explicit operator int(T obj) for default
{
return obj.GetHashCode();
}
}
public extension ExtensionD for <T> TypeA<TypeB<TypeC<T>>> where T : class?
{
public static T StaticExtMethod2() for self
{
UnderlyingTypeStaticMethod1()
UnderlyingTypeStaticMethod2()
self.UnderlyingTypeStaticMethod3()
return ...
}
} Multiple 'for' scopes in an extensionMultiple public extension ExtensionA
{
for <T> List<T> where T : class?
{
public void ExtMethod0(int someArg) for list
{
// "list" is a parameter now.
// You can access its members only by referring to them like 'list.Member'
// T is a type you get from binding when you call it like an instance method
}
public void ExtMethod1(int someArg) for this
{
// "this" is the receiver now.
// You can access its members by referring to them like 'this.Member' OR
// just 'Member' like if they were declared right here in the extension
// T is a type you get from binding when you call it like an instance method
}
public void ExtMethod2<U>(T someArg1, U someArg) for this
{
// "this" is the receiver now.
// You can access its members by referring to them like 'this.Member' OR
// just 'Member' like if they were declared right here in the extension
// T is a type you get from binding when you call it like an instance method
// U is whatever specified at the call site
}
}
for <T> T where T : struct
{
public void ExtMethod3(T val) for [SomeAttribute] ref this
{
// "this" is the receiver now. It is passed by-ref and has [SomeAttribute]
// You can access its members by referring to them like 'list.Member' OR
// just 'Member' like if they were declared right here in the extension
// T is a type you get from binding when you call it like an instance method
this = val;
}
}
for <T> T where T : class?
{
public int ExtProperty for this => 1;
public object this[int i] for this
{
get ... set ...
}
public static T StaticExtMethod() for default
{
return ...
}
public static explicit operator int(T obj) for default
{
return obj.GetHashCode();
}
}
for <T> TypeA<TypeB<TypeC<T>>> where T : class?
{
public static T StaticExtMethod2() for self
{
UnderlyingTypeStaticMethod1()
UnderlyingTypeStaticMethod2()
self.UnderlyingTypeStaticMethod3()
return ...
}
}
}
|
We can introduce a extension_member: ... identifier self_type? '(' parameter_list ')'
generic_type_param_with_self: '<' self_type? generic_type_param '>'
self_type: attribute_list? ('ref' | 'ref readonly' | 'in')? 'self' '?'? '*'?
generic_type_param: ',' attribute_list? identifier generic_type_param?
attribute_list: '[' attribute ']' attribute_list?
attribute: attr_type... ','? public extension TupleExtensions for (int x, int y)
{
public void Swap<ref self>() => (x, y) = (y, x); // `this` is by ref and can be mutated
public int Sum<ref readonly self>() => x + y; // `this` is ref readonly and cannot me mutated
public int Sum<self*>() => this->x + this->y; // `this` is a pointer
}
public extension StringExtension for string
{
public string Reverse() => ...; // instance member
public string Reverse2<self>() => ...; // instance member, but with an explicit self, which is equivalent to `Reverse`
public static string Create() => ...; // static member
public bool IsNullOrEmpty<[NotNullWhen(false)] self?>() => this is null or []; // this is string?
}
public extension GenericExtension<T> for Foo<T> where T : struct
{
public U Bar<[Foo] ref self, U>() where U : class => ...; // some generic example
} Just like extension methods we already have today where we require users to put a I think this should be able to handle all the cases we want to cover. Alternatively, we can still introduce a public extension TupleExtensions for ref (int x, int y)
{
public void Swap() => (x, y) = (y, x); // `this` is by ref and can be mutated
public readonly int Sum => x + y; // `this` is ref readonly and cannot me mutated
}
public extension NullableStringExtension for string?
{
[this:NotNullWhen(false)] public bool IsNullOrEmpty => this is null or [];
}
public extension StringExtension for string
{
public string Reverse() => ...;
} becomes public extension TupleExtensions for (int x, int y)
{
public void Swap(ref self) => (x, y) = (y, x); // `this` is by ref and can be mutated
public int Sum(ref readonly self) => x + y; // `this` is ref readonly and cannot me mutated
}
public extension StringExtension for string
{
public bool IsNullOrEmpty([NotNullWhen(false)] self?) => this is null or [];
public string Reverse(self) => ...;
} and an extension member without public extension StringExtension for string
{
public string Reverse(self) => ...; // instance member
public string Create() => ...; // static member
} However, in this alternative, we don't use |
There's no dedicated thread on disambiguation mechanism but I wanted to bring it up as I just hit this with ToListAsync extension in efcore and mongo driver. I believe the cast operator was considered for properties I think a postfix operator is ideal as the point of an extension is to be able to dot off an expression and chain members. e.(MongoQueryable).ToListAsync() Using golang syntax there to demonstrate but anything similar is much easier to read and write, perhaps opens up the discussion on postfix await as well. |
I believe it had been settled that extensions aren't types that you can declare or cast a value to, so that would mean disambiguation would happen like they do with extension methods today, as a static method invocation, e.g. |
This wouldn't be a proper "cast" in that sense though, I'm referring to this proposal: If that option is no longer on the table, how extension properties are going to be disambiguated? |
I think so, although I guess everything is on the table at the moment. There have been so many meetings/discussions on extensions recently that it all kinda blends, but it sounds like extensions won't be "types" in their own right, and if you can't have a value of the extension type does it make sense to be able to have a pseudocast to an extension type? |
If extensions lower to a static class, the no-cast role is already in place so that wouldn't be something new and is consistent with the actual emitted code.
The proposal does mention this confusion as a drawback so I assume the syntax is not final. I am NOT proposing to add a new cast-operator, but I do think a postfix form works best when talking about extensions. |
We shall see what happens. I assume that extension disambiguation will probably remain a relatively rare thing, so I doubt it would move the needle a whole lot on the existing proposals for postfix cast or await anymore than any of the existing use cases. |
Something being useful, especially when it's not widely used already, shouldn't be enough of a justification to take it into account when designing a new feature - extensions. Given that it's merely a syntactic difference which can account for at most a few extra keystrokes (like the previously discussed Regarding being "useful", there have been requests (at least once or twice that I've seen) to allow things like: string? s = null;
// ...
if (s) // instead of if(s != null)
{
// ...
} The usefulness of the above "implicit not-null-truthiness" is similar to the usefulness of accessing members on a null value, yet C# (rightfully!) rejects this idea. Similarly, I think C# should reject the idea of instance members on null values. Regarding public extension TupleExtensions for ref (int x, int y)
{
public void Swap() => (x, y) = (y, x); // `this` is by ref and can be mutated
public readonly int Sum => x + y; // `this` is ref readonly and cannot me mutated
} I like this a lot, but I do want to point out a minor complication, imagine the below: public extension TupleExtensions for ref (int x, int y)
{
public void SwapAndAdd() => (x, y) = (y + this.Sum, x + this.Sum); // `this` is by ref and can be mutated
}
public extension TupleExtensions for (int x, int y)
{
public int Sum => x + y;
} In the
If this is caused by the requirement that the received interface reference can be reference equal (and "identity equal" in other ways, like TLDR, I think we should go with the syntactically type based approach, and choose the correct emit strategy which satisfies the requirements in the best possible way technically, leaving it to be an implementation detail. Backwards compatibility can be easily achieved by leaving existing extension methods as redirect stubs, and thought-out management of overload resolution. |
Humorously, extensions would enable this. As you'd now be any to add extension conversions to bool, as well as |
PS. Having said "implementation detail", I realized that for other languages to be able to use these new C#-compiled extensions, we may want to spec the emitting strategy after all. |
I thought about that a few days ago myself 🤣 This is less than ideal, but ultimately, it would be a particular team engaging in such shenanigans, and C# cannot possibly stop all the foot-guns out there. |
It's not a foot gun. These conversations exist exactly to allow these scenarios. I know from previous conversations with you that there are things that are allowed that you don't like. But these aren't mistakes, or things that accidentally crept in. They were explicit design decisions to enable these types of scenarios. We had the choice to block them, and we thought about it and explicitly said that we wanted people to be able to do this. Since then people have done this, and everything has been completely fine. The only argument I've seen so far is that someone doesn't like it. But that's an argument for removing practically everything in the language. |
I guess we have a philosophical difference on this subject. I think one of the original objectives of C# to always try creating pits of success and avoid creating pits of failure, is extremely valuable. |
It would require more design work, and more runtime overhead, for the compiler to try to prevent this. People have already decided with extension methods that they want this. I want this, and I do use it. We're not relitigating the concept of extensions, we're extending it. |
I think that is a major design goal. The difference here is that I look at the outcomes of our intentional design decisions and see that they were successful. So why start blocking things that were not a mistake? Note: the same concerns you have here were the ones levied by many in the community about extension methods entirely. The idea that you appear to have instance methods exposed that you did not write was so completely and obviously wrong to some people, it was literally predicted that the feature would cement the death of the language. :-) I look at the flexibility we offered, and I see how the community has used it well. And I don't see a good reason to take away. |
This was strongly considered and discussed at the time. It was not accidental. And there were a few people that thought the same, including wanting null checks automatically emitted. In the end, the benefits of letting methods execute on null far outweighed those concerns. Since then, we've had 20 years to reflect on this and to see how the community would do things. Would they reject such methods. Would they make analyzers that require a null+throw check. In the end, that didn't happen. The community embraced this capability, and they showed that flexibility in extensions could be used to great effect. You may find that a pity. But that's really not selling me on breaking things that have been part and parcel of this design space for decades now :-) |
Note, doing this in a non-breaking way would be challenging. People would absolutely start depending on that, and then we would not be able to change it. So we'd need new syntax for that capability. |
The exact same thing can be written 20 years after
Extensions are a new feature. It's not merely an "extension" (haha) of extension methods. Anyway, I wrote everything there is to write about this. |
I disagree, but even if that were the case I don't see why the team would make a different decision here. I understand that you disagree with the decision, but they (and I) consider it to be a success, thus it would make sense to retain that same capability here. |
Is there going to be a mechanism by which to indicate to nullability analysis that an extension method does not accept null? Or are all extensions providing functionality that doesn't make sense without an instance going to require null guard boilerplate to play well with nullability analysis? |
That already exists with extension methods today. If the extension target is a non-nullable reference type, you'll get a warning if you try to invoke that extension. |
I would be grateful if someone experienced checked my "proposal" here for potential "holes". |
So if I'm reading this right, neither approach will actually let us implement interfaces with extensions? To me that was the primary benefit of dedicated extensions, and without that, I'm not sure this whole thing is worth it. If people are really clamoring for extension properties/operators then a member-only approach seems preferable, ideally as close to the existing extension method syntax as possible. |
This is the request. And it's not a random smattering either. It's teams like the runtime, which very much wants to design apis that take advantage of that :-) |
Dart lang has a good design for extension https://dart.dev/language/extension-methods |
The exercise I've been going through is how existing extension methods would map to any new syntax, and how that lowers to something that doesn't result in either binary or source breaking changes. Particularly cases like |
Without identity preservation your existing code would break badly. Taking this example: Test(new Foo());
void Test(IBar x)
{
if (x is Foo) Console.WriteLine(1);
else Console.WriteLine(2);
}
struct Foo { }
interface IBar { }
extension Bar for Foo : IBar { } If we don't have identity preservation, the code will print |
I disagree. IMO, that code is making a bad assumption and should fail. Rust's behavior is based on this being the only way to implement traits, which is why coherence is necessary in the first place. This is not how extensions need to (or should) work. I don't want the runtime to think that I don't think we're going to agree here as we're approaching it from two very different perspectives. I see extension implementation as a form of type class implementation, where interfaces are the type classes and different implementations can be provided based on scope. I want that ability to say that for this one specific invocation that I want this type to implement this interface in this particular way. To me that feels much more inline with how extensions already work in C#, based on which namespaces are in scope. |
That cannot be the case by definition, because prior to the future addition of extension interface implementations, that
And where does this presumption that it should print 1 come from? The only thing the method And finally, in your example |
It would use scope rules - the extension in scope is used. If both are (say, they are in the same namespace), then it should fail to compile with an ambiguity error.
The thing is, that approach is just impossible with the runtime (at least without enormous unpalatable changes). AFAIK there is no way to "inject" an interface implementation into a type externally. However, let's imagine it is possible somehow. In your example, imagine assembly |
@HaloFour |
That would be one way to implement them. But that would also split the ecosystem.
No, it would result in an This isn't any different to writing a struct wrapper to adapt an existing type to an existing interface today. Nobody expects identity preservation to happen in this case, and I don't see why extension implementation would be any different. I don't want the runtime to be affected as I don't want both the massive blast radius that comes from that and I don't want to limit extensions to only ever supporting exactly one mapping across an entire process with zero concept of scoping. |
I said
All in all, it comes down to the fact that extensions are now a sugar for implementing interfaces through delegation, which is no longer an implementation detail of the feature now. So they aren't really "extensions" any more |
Why would we want to modify the type system in such a manner? |
I don't think type checks should be modified. As soon as you wade into that space you necessitate coherence with all of the problems that come with it. |
It sounds like you are discussing "roles", which is where we strongly see the cases of interface impl, identity, and the rest of the runtime concerns arise. |
We don't need to, others may be doing it already. We pass an extended type to a method which wants an object implementing some interface, how can we be sure that it is not being cast (or type checked) to any class anywhere deeper? What if it's only being cast sometimes and it'll silently break? How we are going to call If an "extension" is just a delegating box, why not just use source generators to make it |
I won't say this is impossible as I know someone has the initial draft and prototype for the runtime change already.
That's why I said we need the orphan rule to recuse, where either the interface or the type being extended should be defined in the current assembly, so that no ambiguity at runtime can even happen in this case. |
They are not, though. There is no way to cast an interface to a class "already" because we don't have extensions yet. Nothing is being broken.
That's just bad code, anybody can write any number of bad type checks and casts with or without extension interfaces. C# cannot prevent people from writing bad code.
That scenario cannot be supported. The runtime does not have a way to make this work (again, without enormous changes which are never happening).
When you write "extending", you put a meaning into it which means natively making the class type have the interface in its implementation hierarchy. That is not the definition of extending, though (as it pertains to this C# proposal). We are extending a type with another interface, just not the way you think extensions should work.
An SG can't do anything here to make it any smoother. Like Halo mentioned earlier, you can already do: class Foo
{
public int Speed;
}
interface IBar
{
void Run(int speed);
}
void Test(IBar x)
{
x.Run(123);
}
void Test<T>(T x) where T : IBar
{
x.Run(123);
}
// we want to make the following code "native" to C# with extensions
struct Bar(Foo foo) : IBar // extension FooBar for Foo : IBar
{
public void Run(int speed)
{
foo.Speed = speed;
}
}
var foo = new Foo();
Test(new Bar(foo)); // Test(foo);
Console.WriteLine(foo.Speed); |
It's one thing to put together a prototype, and quite another to fully test it and ensure nothing else breaks.
That immediately makes the feature all but useless. If we can't extend any type with any interface, it defeats the entire purpose. |
@TahirAhmadov |
If that
Yes - if both extensions are in scope. You can always put them into separate namespaces and avoid having to disambiguate that way.
That's an important thing to clarify here. If |
In other words, this is OK: interface IBar { }
void Test(IBar bar)
{
bar.DoSomething();
DoOtherThings(bar);
if(bar is MyBar myBar)
{
myBar.DoMyThings();
}
} And this is bad: interface IBar { }
void Test(IBar bar)
{
if(bar is MyBar myBar)
{
myBar.DoMyThings();
}
else if(bar is OtherBar otherBar)
{
otherBar.DoOtherThings();
}
}
// instead, it should be:
abstract class BarBase { }
void Test(BarBase bar) // BarBase is a base class, potentially abstract
{
if(bar is MyBar myBar)
{
myBar.DoMyThings();
}
else if(bar is OtherBar otherBar)
{
otherBar.DoOtherThings();
}
else
throw new ArgumentException();
} |
Sorry, that's another case (which I believe should also work, I mentioned it in the earlier post). I mean you can cast extended object to an interface type and pass it to an interface parameter and you can't cast it back to the class and pass it as a class (for a parameter which expects a class) implementing that new interface from your code. |
If it's all your code, why use extensions? Why not just implement the interface on the class itself? |
I believe we're at an impasse. We have different ideas as to how we see extension implementation working which are not compatible with one another. We place different value on identity preservation or flexibility. I, personally, don't want Rust traits and don't find these use cases compelling. From the comment by @CyrusNajmabadi it sounds like there is a different vision for how implementation would work between extensions and roles, and the runtime behavior with identity preservation would possibly come from the latter. |
Because the what extensions really bring us is the full support for ad-hoc polymorphism, where we want to use it to completely replace subtyping polymorphism. |
That is definitely not a goal of extensions. |
Having said all this, it would be nice if the struct wrapper emitted by extensions had support for getting the underlying value: interface IExtension // new inteface in BCL
{
object Object { get; }
}
class Type
{
// ...
public static Type GetTypeWithExtensions(object obj)
{
if(obj is IExtension extension) return extension.Object.GetType();
return obj.GetType();
}
}
class Object
{
// ...
public static bool ReferenceEqualsWithExtensions(object? a, object? b)
{
if(a is IExtension extensionA) a = extensionA.Object;
if(b is IExtension extensionB) b = extensionB.Object;
return ReferenceEquals(a, b);
}
}
// extension FooBar for Foo : IBar
struct Bar(Foo foo) : IBar, IExtension
{
public void Run(int speed)
{
foo.Speed = speed;
}
object IExtension.Object => foo;
}
void Test(IBar bar)
{
var type = Type.GetTypeWithExtensions(bar);
bool refEquals = object.ReferenceEqualsWithExtensions(bar, this._oldBar);
} PS. Obviously, details would need to be worked out, regarding classes vs structs, "nested" extensions (extensions on interface where "instance" is another extension), etc. But I think it should be doable. |
I don't believe there is any plan for extensions to emit a struct wrapper. |
Not even to implement an interface? Does that mean there are no plans to implement interfaces, or there is another way to achieve that without a struct wrapper? |
Yes, it's my code where I define the extension. I want to make it a blanket implementation of my interface. As for third party code expecting an argument which implements an interface, I don't care whether or not it has specialized execution paths for types having a certain base. There may be reasons it was written that way (Hello UnityEngine.Object) These type checks should work like they are expected to work. |
It is far too early to have any stake in teh ground on how an interface would be implemented. Personally, i far prefer that be the space of roles anyways. Personally, If we were to have interface impl, i'd want runtime support. |
Runtime support or not, IMO coherence rules from Rust are much, much too limiting. Immediately you have no solution for any interface defined in any other assembly, which to me is most of the point of extensions. Attempting to relax that would only lead to time-bombs everywhere as, unlike Rust, there's zero way to guarantee that across all of the assemblies that could be loaded that you could only ever have one single implementation between an interface and a given type, not to mention the impact that would have on third party code. Again I look to how Scala manages typeclasses here, as they are a very close model to how I feel extension implementation can and should work. They are incredibly flexible from a developer point of view, especially with scoping. Given most utility typeclasses are defined elsewhere they would be completely useless if coherence were forced upon them. The same is true of nearly all of the utility interfaces in .NET. |
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
The design space for extensions
Let's put the current proposals for extensions in a broader context, and compare their pros and cons.
Background: How we got here
The competing approaches and philosophies around how to express the declaration of extension members trace their roots all the way back to when extension methods were first designed.
C# 3: The start of extension methods
C# 3 shipped with the extension methods we know today. But their design was not a given: An alternative proposal was on the table which organized extension methods in type-like declarations, each for one specific extended (underlying) type. In this model, extension method declarations would look like instance method declarations, and future addition of other member kinds - even interfaces - would be syntactically straightforward. I cannot find direct references to this first type-based proposal, but here is a slightly later version from 2009 that was inspired by it:
Ultimately the current design was chosen for several reasons. Importantly it was much simpler: a syntactic hack on top of static methods. C# 3 was already brimming with heavy-duty features - lambdas, expression trees, query expressions, advanced type inference, etc. - so the appetite to go big on extension methods was limited. Moreover, the static method approach came with its own disambiguation mechanism - just call as a static method! - and allowed convenient grouping of extension methods within one static class declaration. The extension methods of
System.Linq.Enumerable
would have needed to be spread across about 15 extension type declarations if they had been split by underlying type.But perhaps most significantly, we didn't know extension methods were going to be such a hit. There was a lot of skepticism in the community, especially around the risks of someone else being able to add members to your type. The full usefulness of the paradigm was not obvious even to us at the time; mostly we needed them for the query scenario to come together elegantly. So betting on them as a full-fledged new feature direction felt like a risky choice. Better to keep them a cheap hack to start with.
C# 4: Foundered attempts at extension members
Of course extension methods were a huge success in their own right, and the community was immediately asking for more; especially extension properties and extension interfaces. The LDM went to work on trying to generalize to all member kinds, but felt captive to the choices made in C# 3. We felt extension members would have to be a continuation, not just philosophically but syntactically, of the extension methods we'd shipped. For instance, extension properties would have to either use property syntax and take an extra
this
parameter somehow, or we'd need to operate at the lowered level ofset
andget
methods representing the accessors of properties. Here is an example from 2008:These explorations led to proposals of unbearable complexity, and after much design and implementation effort they were abandoned. At the time we were not ready to consider rebooting extensions with an alternative syntax, one that would leave the popular classic extension methods behind as a sort of legacy syntax.
The return of type-based extensions
The Haskell programming language has type classes, which describe the relationships within groups of types and functions, and which, crucially, can be applied after the fact, without those types and functions participating. A proposal from Microsoft Research in Cambridge for adding type classes to C# triggered a string of proposals that eventually led back to extension interfaces: If extension members could somehow help a type implement an interface without the involvement of that type, this would facilitate similar adaptation capabilities to what type classes provide in Haskell, and would greatly aid software composition.
Extension interfaces fit well with the old alternative idea that extensions were a form of type declaration, so much so that we ended up with a grand plan where extensions were types, and where such types would even be a first class feature of their own - separate from the automatic extension of underlying types - in the form of roles.
This approach ran into several consecutive setbacks: We couldn't find a reasonable way to represent interface-implementing extensions in the runtime. Then the implementation of the "typeness" of extensions proved prohibitively expensive. In the end, the proposal had to be pared back to something much like the old alternative design from above: extensions as type declarations, but with no "typeness" and no roles. Here's a recent 2024 example:
We will refer to the resulting flavor of design as "type-based extensions", because the underlying type of the extension is specified on the extension type itself, and the members are just "normal" instance and static member declarations, including providing access to the underlying value with the
this
keyword rather than a parameter.The return of member-based extensions
Now that the bigger story of extensions as types with interfaces has been put on hold with its future prospects in question, it is worth asking: Are we still on the right syntactic and philosophical path? Perhaps we should instead do something that is more of a continuation of classic extension methods, and is capable of bringing those along in a compatible way.
This has led to several proposals that we will collectively refer to as "member-based extensions". Unlike most of the abandoned C# 4 designs of yore, these designs do break with classic extension methods syntactically. Like the type-based approach they embrace an extension member declaration syntax that is based on the corresponding instance member declaration syntax from classes and structs. However, unlike type-based extensions, the underlying type is expressed at the member level, using new syntax that retains more characteristics of a parameter.
Here are a few examples from this recent proposal:
The motivation is not just a closer philosophical relationship with classic extension methods: It is an explicit goal that existing classic extension methods can be ported to the new syntax in such a way that they remain source and binary compatible. This includes allowing them to be called as static methods, when their declarations follow a certain pattern.
We've had much less time to explore this approach. There are many possible syntactic directions, and we are just now beginning to tease out which properties are inherent to the approach, and which are the result of specific syntax choices. Which leads us to the following section, trying to compare and contrast the two approaches.
Comparing type-based and member-based proposals
Both approaches agree on a number of important points, even as the underlying philosophy differs in what currently feels like fundamental ways:
extension
orextensions
) to hold extension member declarations. Neither approach keeps extension members in static classes.And of course both approaches share the same overarching goal: to be able to facilitate extension members of nearly every member kind, not just instance methods. Either now or in the future this may include instance and static methods, properties, indexers, events, operators, constructors, user-defined conversions, and even static fields. The only exception is members that add instance state, such as instance fields, auto-properties and field-like events.
The similarities make it tempting to search for a middle ground, but we haven't found satisfactory compromise proposals (though not for lack of trying). Most likely this is because the differences are pretty fundamental. So let's look at what divides the two approaches.
Relationship to classic extension methods
The core differentiating factor between the two approaches is how they relate to classic extension methods.
In the member-based approach, it is a key goal that existing classic extension methods be able to migrate to the new syntax with 100% source and binary compatibility. This includes being able to continue to call them directly as static methods, even though they are no longer directly declared as such. A lot of design choices for the feature flow from there: The underlying type is specified in the style of a parameter, including parameter name and potential ref-kinds. The body refers to the underlying value through the parameter name.
Only instance extension methods declared within a non-generic
extensions
declaration are compatible and can be called as static methods, and the signature of that static method is no longer self-evident in the declaration syntax.The type-based approach also aims for comparable expressiveness to classic extension methods, but without the goal of bringing them forward compatibly. Instead it has a different key objective, which is to declare extension members with the same syntax as the instance and static members they "pretend" to be, leaving the specification of the underlying type to the enclosing type declaration. This "thicker" abstraction cannot compatibly represent existing classic extension methods. People who want their existing extension methods to stay fully compatible can instead leave them as they are, and they will play well with new extension members.
While the type-based approach looks like any other class or struct declaration, this may be deceptive and lead to surprises when things don't work the same way.
The member-based approach is arguably more contiguous with classic extension methods, whereas the type-based approach is arguably simpler. Which has more weight?
Handling type parameters
An area where the member-based approach runs into complexity is when the underlying type is an open generic type. We know from existing extension methods that this is quite frequent, not least in the core .NET libraries where about 30% of extension methods have an open generic underlying type. This includes nearly all extension methods in
System.Linq.Enumerable
andSystem.MemoryExtensions
.Classic extension methods facilitate this through one or (occasionally) more type parameters on the static method that occur in the
this
parameter's type:The same approach can be used to - compatibly - declare such a method with the member-based approach:
We should assume that open generic underlying types would be similarly frequent for other extension member kinds, such as properties and operators. However, those kinds of member declarations don't come with the ability to declare type parameters. If we were to declare
AsSpan
as a property, where to declare theT
?This is a non-issue for the type-based approach, which always has type parameters and underlying type on the enclosing
extension
type declaration.For the member-based approach there seem to be two options:
Both lead to significant complication:
Type parameters on non-method extension members
Syntactically we can probably find a place to put type parameters on each kind of member. But other questions abound: Should these be allowed on non-extension members too? If so, how does that work, and if not, why not? How are type arguments explicitly passed to each member kind when they can't be inferred - or are they always inferred?
This seems like a big language extension to bite off, especially since type parameters on other members isn't really a goal, and current proposals don't go there.
Allow type parameters and underlying type to be specified on the enclosing type declaration
If the enclosing
extensions
type declaration can specify type parameters and underlying type, that would give members such as properties a place to put an open generic underlying type without themselves having type parameters:This is indeed how current member-based proposals address the situation. However, this raises its own set of complexities:
extensions
declaration starts carrying crucial information for at least some scenarios.extensions
declaration specifies an underlying type, it can no longer be shared between extension members with different underlying types. The grouping of extension members with different underlying types that is one of the benefits of the member-based approach doesn't actually work when non-method extension members with open generic underlying types are involved: You need separateextensions
declarations with separate type-level underlying types just as in the type-based approach!In summary, classic extension methods rely critically on static methods being able to specify both parameters and type parameters. A member-based approach must either extend that capability fully to other member kinds, or it must partially embrace a type-based approach.
Tweaking parameter semantics
An area where the type-based approach runs into complexity is when the default behavior for how the underlying value is referenced does not suffice, and the syntax suffers from not having the expressiveness of "parameter syntax" for the underlying value.
This is a non-issue for the member-based approach, which allows all this detail to be specified on each member.
There are several kinds of information one might want to specify on the underlying value:
By-ref or by_value for underlying value types
In classic extension methods, the fact that the
this
parameter is a parameter can be used to specify details about it that real instance methods don't get to specify about howthis
works in their body. By default,this
parameters, like all parameters, are passed by value. However, if the underlying type is a value type they can also be specified asref
,ref readonly
andin
. The benefit is to avoid copying of large structs and - in the case ofref
- to enable mutation of the receiver itself rather than a copy.The use of this varies wildly, but is sometimes very high. Measuring across a few different libraries, the percentage of existing extension methods on value types that take the underlying value by reference ranges from 2% to 78%!
The type-based approach abstracts away the parameter passing semantics of the underlying value - extension instance members just reference it using
this
, in analogy with instance members in classes and structs. But classes and structs have different "parameter passing" semantics forthis
! In classesthis
is by-value, and in structsthis
is byref
- orref readonly
when the member or struct is declaredreadonly
.There are two reasonable designs for what the default should be for extension members:
this
by value, and when it is a value type passthis
byref
(or perhapsref readonly
when the member isreadonly
). In the rare case (<2%) that the underlying type is an unconstrained type parameter, decide at runtime.this
by value.Either way, the default will be wrong for some significant number of extension members on value types! Passing by value prevents mutation. Passing by reference is unnecessarily expensive for small value types.
In order to get to reasonable expressiveness on this, the type-based approach would need to break the abstraction and get a little more "parameter-like" with the underlying type. For instance, the
for
clause might optionally specifyref
orin
:Attributes
This-parameters can have attributes. It is quite rare (< 1%), and the vast majority are nullability-related. Of course, extension members can have attributes, but they would need a way to specify that an attribute goes on the implicit
this
parameter!One way is to introduce an additional attribute target, say
this
, which can be put on instance extension members:Nullable reference types
A classic extension method can specify the underlying type as a nullable reference type. It is fairly rare (< 2%) but allows for useful scenarios, since, unlike instance members, extension members can actually have useful behavior when invoked on null. Anotating the receiver as nullable allows the extension method to be called without warning on a value that may be null, in exchange for its body dealing with the possibility that the parameter may be null.
A type-based approach could certainly allow the
for
clause to specify a nullable reference type as the underlying type. However, not all extension members on that type might want it to be nullable, and forcing them to be split across twoextension
declarations seems to break with the ideal that nullability shouldn't have semantic impact:It would be better if nullability could be specified at the member level. But how? Adding new syntax to members seems to be exactly what the type-based approach is trying to avoid! The best bet may be using an attribute with the
this
target as introduced above:This would allow extension members on nullable and nonnullable versions of the same underlying reference type to be grouped together.
Grouping
Classic extension methods can be grouped together in static classes without regard to their underlying types. This is not the case with the type-based approach, which requires an
extension
declaration for each underlying type. Unfortunately it is also only partially the case for the member-based approach, as we saw above. Adding e.g. extension properties toMemoryExtensions
, which has a lot of open generic underlying types, would lead to it having to be broken up into severalextensions
declarations.This is an important quality of classic extension methods that unfortunately neither approach is able to fully bring forward.
Non-extension members
Current static classes can of course have non-extension static members, and it is somewhat common for those to co-exist with extension methods.
In the member-based approach a similar thing should be easy to allow. Since the extension members have special syntactic elements, ordinary static members wouldn't conflict.
In the type-based approach, ordinary member syntax introduces extension members! So if we want non-extension members that would have to be accommodated specially somehow.
Interface implementation
The type-based syntax lends itself to a future where extensions implement interfaces on behalf of underlying types.
For the member-based syntax that would require more design.
Summary
All in all, both approaches have some challenges. The member-based approach struggles with open generic underlying types, which are fairly common. They can be addressed in two different ways, both of which add significant syntax and complexity.
The type-based approach abstracts away parameter details that are occasionally useful or even critical, and would need to be augmented with ways to "open the lid" to bring that expressiveness forward from classic extension methods when needed.
The text was updated successfully, but these errors were encountered: