-
Notifications
You must be signed in to change notification settings - Fork 1k
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
[Proposal]: Extensions #5497
Comments
I prefer using public extension Foo of int : IA, IB, IC, ...
{
...
} Otherwise it will be too confusing if you are extending an interface: public extension Foo : IA, IB, IC { } vs public extension Foo of IA : IB, IC { }
|
I'm curious as to how the team weighs the relative benefits between "roles" and "extension implementation". It feels that without some additional effort in the runtime the two are somewhat incompatible with each other, so if those differences can't be reconciled which of the features might the team lean towards? Personally, I find extension implementation much more exciting than roles, but that's just my opinion. |
@hez2010 public extension Foo for IA : IB, IC { } |
Who gave you an early preview of my notes? They're up now, discussion at #5500. |
Here's a scenario that will be great fun to try to accommodate in the design: interface IFoo { }
interface IBar { }
class Thing { }
public extension FooThing for Thing : IFoo { }
public extension BarThing for Thing : IBar { }
void Frob<T>(T t) where T : IFoo, IBar { }
Frob(new Thing()); On an unrelated bikeshedding note, what about using the existing reserved keywords |
@sab39 Given, as you've mentioned, how similar these two concepts are. I too am looking for a good syntactic way to convey that similarity, with a clear way to do indicate in which way they differ. Thanks for the |
I'm not sure if I should re-post my comments from the discussion here?
This is complicated, but doable using current constraints of the framework. An anonymous type can be generated: class <mangled>Thing_IFoo_IBar : IFoo, IBar
{
internal <mangled>Thing_IFoo_IBar(Thing thing) { this._thing = thing; }
readonly Thing _thing;
void IFoo.Foo() { ... } // these member(s) are copied from, or call into, FooThing
void IBar.Bar() { ... } // these member(s) are copied from, or call into, BarThing
}
Frob(new <mangled>Thing_IFoo_IBar(new Thing())); The same can be done for generic types, etc. Yes, it's complicated, but unlike roles, it's very possible. |
This was just one example. It's not the main motivation. We discussed in the LDM that there were definitely plenty of scenarios where you'd still want adapters in a strongly typed way that would be sensible. |
@TahirAhmadov That works, more or less, for the specific example I gave, but what if |
If it's not the main motivation, surely it shouldn't be the one discussed in the OP, should it? |
The OP is simply showing a demonstration. This is a broad topic and we need to spend a ton more time on it prior to even getting close to a place where we could write something up that was fully fleshed out and chock full of examples and whatnot. |
The |
Back with .NET Framework, I've often ran into situations where i wanted a The only thing I don't quite get is why we need two keywords here, |
That's the thing, it would be very interesting to see an example which would demonstrate how |
That's fine. It's something we're working on at this moment '-). The point was raised and was something we intend to get to and write more on. I def don't want us to get the impression that it's just for that. Thanks! |
Roles feel like they need a validator method, something that is invoked to by the "implicit conversion" to ensure that the underlying object can fill in that role. I'm not even sure the conversion should be implicit. I'm sure it will be annoying to do stuff like |
Hmm, that almost makes it sound like you want Extension DUs... |
I don't want them to be a DU per se, it's more similar to getting an |
@orthoxerox F# has a feature Partial Active Patterns which looks somewhat like your idea. |
C# isn't the only language on CoreCLR, without runtime support how would you expect roles to be defined and used in other languages? Other languages don't recognize the mangled anonymous class. |
The pseudocode I wrote was specifically for extensions, not roles. class Thing { }
interface IFoo { void Foo(); }
extension FooThing: Thing, IFoo { void Foo() { ... } }
void Frob(IFoo foo) { }
// this line:
Frob(new Thing());
// is compiled to this:
class <mangled>Thing_IFoo : IFoo
{
internal <mangled>Thing_IFoo(Thing thing) { this._thing = thing; }
readonly Thing _thing;
void IFoo.Foo() { ... } // these member(s) are copied from, or call into, FooThing
}
Frob(new <mangled>Thing_IFoo(new Thing())); |
I also want to voice that I wish there would be some keyword being reused instead of casting new keyword Or Aside from that I have nothing against, and fully support this issue |
Keywords can be introduced as contextual keywords so it can be made not to introduce breaking changes. |
@hez2010 I know there is no breaking change but it still should be the last option to introduce any new keyword. If there would be any possible for composite or reuse then we should |
I found the idea of |
I don't get why I'll just copy/paste my comment from the other post so something like this: // Customer.cs
namespace Data;
public extension Customer : DataObject // Wrapper type
{
public string Name => this["Name"].AsString();
public string Address => this["Address"].AsString();
public IEnumerable<Order> Orders => this["Orders"].AsEnumerable();
}
// JsonDataObject.cs
namespace Data;
using JsonLibrary;
public extension JsonDataObject : DataObject // Extension type
{
public string ToJson() { … this … }
public static T FromJson<T>(string json) { … }
}
// Program.cs / Main method
using Data;
using Data.JsonDataObject; // Importing a specific extension type
using Data.*; // Importing all extensions types in the namespace Data
var customer = customer.FromJson<Customer>(args[0]);
WriteLine(customer.ToJson()); |
Would this be allowed under roles? role Foo<T> : T
where T : ISomeInterface
{
} or would we be forced to directly extend the interface and bring in boxing conversions all over the place as we implicitly cast back and forth in a generic function? |
Thinking about it, I imagine this happening: class Thing { }
interface IFoo { void Foo(); }
// the following line
public extension FooThing: Thing, IFoo { void Foo() { ... } }
// is compiled to:
// these attributes are once per assembly, similar to NRT attributes
class ExtensionTypeAttribute { public ExtensionTypeAttribute(params Type[] types) { ... } ... }
class ExtensionInstanceMemberAttribute { }
class ExtensionStaticMemberAttribute { }
// the actual extension becomes:
[ExtensionType(typeof(Thing), typeof(IFoo))]
public static class FooThing
{
[ExtensionInstanceMember]
public static void Foo(Thing @this) { ... }
}
void Frob(IFoo foo) { }
// this line:
Frob(new Thing());
// is compiled to this:
class <mangled>Thing_IFoo : IFoo
{
internal <mangled>Thing_IFoo(Thing thing) { this._thing = thing; }
readonly Thing _thing;
void IFoo.Foo() { FooThing.Foo(this._thing); }
}
Frob(new <mangled>Thing_IFoo(new Thing())); |
Everything is currently in flux :-) |
It's not how type classes work. Identity preservation is pretty important when it involves interface implementation, otherwise, any implicit extension coming into the scope can simply break your existing code by accident. While it's too early to talk about it so I am not able to speak more about it, there are already some initial thoughts about this feature which adds a lookup table to the metadata and looks up it at runtime to make it work. |
@hez2010 how so? Please elaborate on the "break your existing code by accident" part. |
Let's say you have a dictionary for value cache per type Dictionary<Type, object> dict;
bool TryGetValue(Type key, out object result) => ...; Now you may have a method void UpdateValue<T>(T value) where T : IFoo
{
dict[value.GetType()] = value;
// or even dict[typeof(T)] = value;
} If the type identity cannot be preserved, let's say now you have an implicit extension UpdateValue(42);
TryGetValue(typeof(int), out var result); // it will return false instead of true! And you don't have any way to "opt-out" this extension if the using is imported by Another typical use case can be CsWinRT where one can extend an interface IWinRTMarshallingDescriptor<T, TAbi>
where T : IWinRTMarshallingDescriptor<T, TAbi>
{
static abstract T? FromAbi(TAbi value);
static abstract TAbi FromManaged(T? value);
// ...
} If the type identity cannot be preserved, the above code will result in undefined behavior. |
@hez2010 The If there was another overload, All this leaving aside the fact that this is a very contrived scenario. PS. I haven't done anything with WinRT so I will not comment on that. PPS. My main argument here is that if you have a method which expects void UpdateValue(object value)
{
dict[value.GetType()] = value;
} |
That's not correct. The extension may come from assembly A, and the Type identity preservation is not necessary in extensions without interface implementation, but an extension implementing interfaces for existing type will become type classes, where type identity does matter. |
It doesn't matter what assembly each part of this code is in.
|
I think that this is a good conversation for a separate discussion, or for Discord, given extension implementation is not planned for C# 13.0 and the design is still completely up in the air. As they are considered to be type classes, it feels worthwhile to consider how type classes are used and are expected to behave in other languages. |
Yeah. Extensions implementing interfaces is type classes which is a different feature from extensions, and here it only shares the syntax with extensions. People will start to rely on extensions implementing interfaces for dynamic ad-hoc polymorphism, where type identity preservation becomes important. The feature is out-of-scope for C# 13.0, we may need to see how type classes work in other languages before talking about the implementation. I believe metadata and some runtime/reflection APIs will also need to be updated to support it though. |
Is there an implementation reason for why pointers, ref structs, and ref types can't be used as the underlying type? With the team pursuing a static method approach to emitting extensions, this restriction appears arbitrary. |
@ds5678 I'd have to assume restrictions are due to a "get it working now, and iron out the details later" approach. |
@ds5678 : what would an extension on a pointer do? |
In interop scenarios, pointers are often used as typed references to native objects. |
Will extensions (not just interface additions, any part of them) not be shipped with C# 13? |
Coming back to this, if the enum members were not initialized, and the enum was extended more than once (in more than one file), how would the values be assigned? Perhaps both the extended and extending type should be required to specify constant values, so that the values are deterministic? |
I mean, you could almost certainly write this, even without explicit support for adding new enum members (whether you should is another question): public implicit extension ConsoleColorExtensions for ConsoleColor
{
public const ConsoleColor NewColor = (ConsoleColor)71;
} |
I have a problem. To avoid the extension of vector 2D affecting vector 3D, we should specify the type of extension to be applied. |
This is probably further down the line but I think this could solve record custom equality for lists, public implicit extension EnumerableEquitableExtension<T> : IEquitable<IEnumerable<T>> for IEnumerable<T>
where T : IEquitable<T>
{
public bool Equals(IEnumerable<T> other) => this.SequenceEqual(other);
} That's assuming |
I don't imagine that it could? Given that property is already compiled there's nowhere for the extension binding to occur. You'd need some kind of runtime registry of extensions (or implementations) per type, I'd think. However, if the goal was specifically to support that for equality comparison of the fields of a record, the language could have always supported mechanisms to provide custom equality comparers, it's just something that the language team intentionally didn't want to do. I don't see this as a way to backdoor that decision, given I think it'd be a much messier approach. |
I think that's a question of its own. If these interfaces are supported by |
I doubt it. |
Agreed. If you want a custom |
My point was that this is not about EqualityComparer specifically. Extending |
I totally agree, it is an interesting conversation. The real question is can it be done efficiently without effectively making all type checks that much more expensive. I assume it wouldn't affect already compiled code, but maybe the compiler could emit additional type checks for extensions that are in scope? // given
public interface IFoo { }
public extension Int32FooExtension for int : IFoo { }
object o = 123;
// then
if (o is IFoo foo) { }
// lowers to
if (o is IFoo foo) {
// already is IFoo
}
// otherwise enumerate extensions in scope that implement IFoo
if (o is int $temp) {
IFoo foo = new Int32FooExtension($temp);
} |
In Rust there's a whole concept of trait objects to support this by capturing anything about the trait impl at the time of assignment. Generic contraints are different because the information is available statically so naturally there's no performance penalty if used that way, otherwise you get a complete different codegen. |
Well, there can be a penalty. It just depends on if it's ok where that penalty happens. In rust, this penalty often happens at initial compile time, as well as with potentially huge code-size explosion. That's often the right tradeoff for users, but it's not universally so. A lot of people have a distaste about runtime costs. But practically speaking, it's usually totally ok, and extremely high perf code doesn't care anyways, since it's not using tehse operations to begin with as even the stock behavior today is too costly for them. |
I'm against implicit extensions affecting EqualityComparer.Default. That feels like spooky action at a distance. Only explicit extensions or extensions used as type parameters should affect it. Okay: EqualityComparer<ExplicitExtension>.Default
EqualityComparer<ImplicitExtension>.Default
EqualityComparer<T>.Default // where an extension is used as the type argument Not okay: EqualityComparer<BaseType>.Default |
I don't know a whole lot about Rust but reading a bit about implementation and coherence it seems Rust also forbids the ability to provide an implementation for an external trait to an external type. So this kind of behavior wouldn't be permitted in Rust anyway, and that is intentional so that you cannot change the meaning of existing code. It also prevents the problem of having multiple implementations for the same trait to the same type. That's a very different design from extensions where the point of extension implementations would be to extend external types, and extensions can be scoped by namespace. The idea of extensions affecting the runtime behavior of code is certainly an interesting conversation to have, but I think there are several cans of worms there. |
Is that possible to use extension to create COM/WinRT wrapper? // Such as we have a COM interface.
[GeneratedComInterface, Guid("70C65787-A406-49A8-938C-CE8CBBD26421")]
public partial interface IInterface
{
void Method();
}
// And we have a class in some place.
public class Class
{
public void Method()
{
// do something...
}
}
#if Expect
// Then we can use extension to implement COM/WinRT interface for the class.
[GeneratedComClass]
public partial extension CoClass for Class : IInterface;
#else
// Which should write like this now.
[GeneratedComClass]
public partial class CoClass(Class inner) : IInterface
{
public void Method() => inner.Method();
}
#endif Now I need to write like this: https://github.com/wherewhere/SelfCOMServer/blob/main/SelfCOMServer/Common/RemoteProcess.cs [GeneratedComClass]
public partial extension class CoClass for Class : IInterface; |
Discussed in #5496
Originally posted by MadsTorgersen November 30, 2021
Extensions
LDM Meetings
https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-12-01.md#roles-and-extensions
https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-08-31.md#roles
https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-09-26.md#roles--extensions
https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-22.md#extensions
https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-12-11.md#extensions
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-02-28.md#extensions
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-06-12.md#extensions
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-06-26.md#extensions
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-07-22.md#extensions
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-18.md#extensions-naming
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-30.md
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-10-02.md#extensions
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-10-07.md
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-10-09.md
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-10-14.md
The text was updated successfully, but these errors were encountered: