-
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]: Always available extension methods #4029
Comments
Could this proposal made somehow compatible with F#'s |
I'm interested enough in this to see what the rest of the LDM thinks. There are certainly times that this could be useful in Roslyn. |
For me However, since everyone will be encouraged to put extension methods in the same namespace as the type, vs. I've seen libraries define extension methods on System types (for internal usages) and make them public! Today this is not much problem because those are in a different namespace, but if they put them in the same namespace as the type, all of those extensions will be suddenly in scope and there is no easy way to favor one over the another. |
@alrz An alternative would be to mark an extension method as always in scope explicitly using an attribute or similar. If the compiler is able to do this efficiently that would be ideal - the risk is that the lookup set becomes huge for every single extension method. In practice I don't know how much of a problem that would be. Finding the extension methods could be quick since the compiler can cache the location of all such extension methods. Processing them would be slower, but since most will have the wrong name, the actual candidate set that needs to be looked at could be of a reasonable size. |
If you're going to opt-in, global imports would do the same. There's a limitation, however, currently you can't add a specific static class for its extensions e.g. |
What do you mean by global imports? |
I mean opt in by the author of the extension method, not the consumer |
#3428 (either a cli option as proposed, or something like #3428 (comment))
That just restrict what's possible. Personally I'd prefer if we just relax Not sure if we want attributes to affect binding anyways. |
@Unknown6656 |
In the three use cases I suggested here, it would be better for the library author to control this than the consumer. I want people to just install my extension GetEnumerator and be done, not have to edit the csproj to add a global import. |
You could include the global import from a custom |
Also discussed here: dotnet/roslyn#7489 |
How about an attribute on the extension class like Or maybe something like |
I can relate to the problem described here. That said, should we be looking more towards default implemented interfaces to solve this problem perhaps? E.g.: public interface IContainer<T> : IDisposable
{
public TResult Run<TResult, TParam>(Func<T, TParam, TResult> func, TParam param);
public TResult Run<TResult>(Func<T, TResult> func)
=> Run((t, func) => func(t), func);
public void Run(Action<T> action)
=> Run((t, action) =>
{
action(t);
return default(object);
}, action);
} |
@egil Default interface methods won't work here since they require casting the container to the interface. Extension methods solve that issue. |
Indeed, if the users have a reference to a concrete type and not an interface. It's one of my complaints about default implemented interfaces, one that would be great if was addressed. |
Isn't this already solved now that we have global usings and implicit usings via the csproj (and nuget packages)? |
@siegfriedpammer no. The point of this proposal is that the consumer doesn't need to do anything extra, not that there is no using in the file. The idea is that once you have an instance of a type (no matter how you got it) that you can call extensions on it from the same namespace. Thanks! |
As an improvement for the global namespace workaround, I name my extension classes differently than other classes. For |
|
This proposal is from 2020 and there is no point in ressurecting it now, since we can already use |
Always available extension methods
Summary
Currently it is impossible to make sure that an extension method is always in scope, except by putting it in the global namespace. We add a step to lookup where we look for extension methods in the same namespace as the receiver type.
Motivation
There are a number of cases where in order for extension methods to be useful we want them to be available under all circumstances, without having to import anything. In such cases the only option is to put them in the global namespace which clogs up the global namespace.
Here are 3 examples where I've wanted this:
Patterns
Many C# features are available if a type structurally matches a certain shape, and that shape can be provided by extension methods. For example
GetAwaiter
,Deconstruct
, andGetEnumerator
can all be extension methods, and allow you toawait
, deconstruct, andforeach
a type respectively.When providing such extension methods, you want usage of the feature to feel native. You don't want to have to import a namespace to have this work. For this reason I put my extension
GetEnumerator
onSystem.Range
in the global namespace, allowing you toforeach (var i in 1..10)
without having to import anything.Also for these patterns code fixers to import the required extension method have to be implemented manually each time a new such pattern is added, and it can lag behind, meaning discoverability of these extension methods can be poor.
The extension method is a core part of the API.
In StrongInject the
IContainer
interface is defined as follows:The single method that needs to be implemented is quite complex in order to maximize performance where necessary. Also since a container often implements
IContainer<T>
for multiple types, the implementations are often implicit, and hence awkward to call.The solution to this has been to provide a number of extension methods. This makes it easy to call implicit implementations, and also provides more convenient but less performant API's wrapping the core API:
These extension methods are meant to be the publicly visible API of the container. If you have access to the container, I want these extension methods to be clearly visible, because they are how you are meant to interact with it, not the method actually defined on
IContainer
. This unfortunately doesn't work if you haven't importedStrongInject
, and you will find the API very difficult to use. Since aRun
method does exist onIContainer
tooling wont even help you import the extension methods.Analyzers
There are a number of analyzers which warn if a type has a method, and it isn't called - for example https://www.nuget.org/packages/ConfigureAwaitChecker.Analyzer/ which makes sure you call
ConfigureAwait(false)
on any type which defines it.Most of these check for the type using normal lookup rules. This means that if you define an extension
ConfigureAwait
, and it isn't imported you wont get a warning - potentially causing deadlocks and all other sorts of nastiness in your code. The safest solution is to put the extension method in the global namespace.Detailed design
If we fail to find any extension methods under current rules, we look for any extension methods in the same namespace as the namespace of the receiver type.
This will allow us to put any extension methods we want to always be available in the same namespace as the type they extend, meaning they will almost always be available.
We update the spec as follows:
Drawbacks
This will work well for my Range Foreach use case and my ConfigureAwait use case but less well for the StrongInject use case. For Stronginject, if I have an instance of type
IContainer<T>
this rule will bring the extension methods into scope (which is useful since they are more convenient), but if I have an instance of typeSomeNamespace.SomeContainer : IContainer<T>
it will not.This will encourage people to put extension methods in the same namespace as the type they extend. Since it's very common to import the namespace of a type when you use a type, this will make it very awkward to call other, conflicting, extension methods defined in other namespaces.
In practice I think there's 3 sorts of places extension methods can be defined:
a. As part of the core API
b. In a nuget package focused just on providing these extension methods.
c. In a package imported for other purposes which happens to define some extension methods.
Scenario a isn't much of a problem, because it should be possible for an API to define it's own extension methods, and other people shouldn't be hiding them.
Scenario b also isn't much of a problem since if you don't want to use those provided extension methods, you shouldn't import the package.
c. is more problematic, but it is already bad practice to define public extension methods in a package designed for other purposes. It is doubly so to do so in the same namespace as the receiver. We need to make sure to put this message across strongly: do not put extension methods in the same namespace as the receiver just because. Do it if there's a very strong reason why these extension methods should always be available.
Alternatives
You can put the extension method in the global namespace. However this quickly clutters up the glabal namespace - one my extension methods is defined in a class called
ContainerExtensions
anotherRangeExtensions
- hardly the most generic of names. How long till two people put a class just calledExtensions
in the global namespace, and then it is impossible to call either?We could alternatively add some marker such as
[AlwaysAvailableExtension]
to mark extension methods that should be available everywhere. This would help with the StrongInject case, but may negatively impact compiler performance.Unresolved questions
Design meetings
https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-11-11.md#always-available-extension-methods
The text was updated successfully, but these errors were encountered: