Skip to content
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]: Static abstract members in interfaces #4436

Open
1 of 4 tasks
Tracked by #829 ...
MadsTorgersen opened this issue Feb 12, 2021 · 579 comments
Open
1 of 4 tasks
Tracked by #829 ...

[Proposal]: Static abstract members in interfaces #4436

MadsTorgersen opened this issue Feb 12, 2021 · 579 comments
Assignees
Labels
Implemented Needs ECMA Spec This feature has been implemented in C#, but still needs to be merged into the ECMA specification Proposal champion Proposal
Milestone

Comments

@MadsTorgersen
Copy link
Contributor

MadsTorgersen commented Feb 12, 2021

Static abstract members in interfaces

  • Proposed
  • Prototype: Not Started
  • Implementation: Not Started
  • Specification: Not Started

Speclet: https://github.com/dotnet/csharplang/blob/main/proposals/csharp-11.0/static-abstracts-in-interfaces.md

Summary

An interface is allowed to specify abstract static members that implementing classes and structs are then required to provide an explicit or implicit implementation of. The members can be accessed off of type parameters that are constrained by the interface.

Motivation

There is currently no way to abstract over static members and write generalized code that applies across types that define those static members. This is particularly problematic for member kinds that only exist in a static form, notably operators.

This feature allows generic algorithms over numeric types, represented by interface constraints that specify the presence of given operators. The algorithms can therefore be expressed in terms of such operators:

// Interface specifies static properties and operators
interface IAddable<T> where T : IAddable<T>
{
    static abstract T Zero { get; }
    static abstract T operator +(T t1, T t2);
}

// Classes and structs (including built-ins) can implement interface
struct Int32 :, IAddable<Int32>
{
    static Int32 I.operator +(Int32 x, Int32 y) => x + y; // Explicit
    public static int Zero => 0;                          // Implicit
}

// Generic algorithms can use static members on T
public static T AddAll<T>(T[] ts) where T : IAddable<T>
{
    T result = T.Zero;                   // Call static operator
    foreach (T t in ts) { result += t; } // Use `+`
    return result;
}

// Generic method can be applied to built-in and user-defined types
int sixtyThree = AddAll(new [] { 1, 2, 4, 8, 16, 32 });

Syntax

Interface members

The feature would allow static interface members to be declared virtual.

Today's rules

Today, instance members in interfaces are implicitly abstract (or virtual if they have a default implementation), but can optionally have an abstract (or virtual) modifier. Non-virtual instance members must be explicitly marked as sealed.

Static interface members today are implicitly non-virtual, and do not allow abstract, virtual or sealed modifiers.

Proposal

Abstract virtual members

Static interface members other than fields are allowed to also have the abstract modifier. Abstract static members are not allowed to have a body (or in the case of properties, the accessors are not allowed to have a body).

interface I<T> where T : I<T>
{
    static abstract void M();
    static abstract T P { get; set; }
    static abstract event Action E;
    static abstract T operator +(T l, T r);
}

Open question: Operators == and != as well as the implicit and explicit conversion operators are disallowed in interfaces today. Should they be allowed?

Explicitly non-virtual static members

Todau's non-virtual static methods are allowed to optionally have the sealed modifier for symmetry with non-virtual instance members.

interface I0
{
    static sealed void M() => Console.WriteLine("Default behavior");
    
    static sealed int f = 0;
    
    static sealed int P1 { get; set; }
    static sealed int P2 { get => f; set => f = value; }
    
    static sealed event Action E1;
    static sealed event Action E2 { add => E1 += value; remove => E1 -= value; }
    
    static sealed I0 operator +(I0 l, I0 r) => l;
}

Implementation of interface members

Today's rules

Classes and structs can implement abstract instance members of interfaces either implicitly or explicitly. An implicitly implemented interface member is a normal (virtual or non-virtual) member declaration of the class or struct that just "happens" to also implement the interface member. The member can even be inherited from a base class and thus not even be present in the class declaration.

An explicitly implemented interface member uses a qualified name to identify the interface member in question. The implementation is not directly accessible as a member on the class or struct, but only through the interface.

Proposal

No new syntax is needed in classes and structs to facilitate implicit implementation of static abstract interface members. Existing static member declarations serve that purpose.

Explicit implementations of static abstract interface members use a qualified name along with the static modifier.

class C : I<C>
{
    static void I.M() => Console.WriteLine("Implementation");
    static C I.P { get; set; }
    static event Action I.E;
    static C I.operator +(C l, C r) => r;
}

Open question: Should the qualifying I. go before the operator keyword or the operator symbol + itself? I've chosen the former here. The latter may clash if we choose to allow conversion operators.

Semantics

Operator restrictions

Today all unary and binary operator declarations have some requirement involving at least one of their operands to be of type T or T?, where T is the instance type of the enclosing type.

These requirements need to be relaxed so that a restricted operand is allowed to be of a type parameter that is constrained to T.

Open question: Should we relax this further so that the restricted operand can be of any type that derives from, or has one of some set of implicit conversions to T?

Implementing static abstract members

The rules for when a static member declaration in a class or struct is considered to implement a static abstract interface member, and for what requirements apply when it does, are the same as for instance members.

TBD: There may be additional or different rules necessary here that we haven't yet thought of.

Interface constraints with static abstract members

Today, when an interface I is used as a generic constraint, any type T with an implicit reference or boxing conversion to I is considered to satisfy that constraint.

When I has static abstract members this needs to be further restricted so that T cannot itself be an interface.

For instance:

// I and C as above
void M<T>() where T : I<T> { ... }
M<C>();  // Allowed: C is not an interface
M<I<C>>(); // Disallowed: I is an interface

Accessing static abstract interface members

A static abstract interface member M may be accessed on a type parameter T using the expression T.M when T is constrained by an interface I and M is an accessible static abstract member of I.

T M<T>() where T : I<T>
{
    T.M();
    T t = T.P;
    T.E += () => { };
    return t1 + T.P;
}

At runtime, the actual member implementation used is the one that exists on the actual type provided as a type argument.

C c = M<C>(); // The static members of C get called

Drawbacks

  • "static abstract" is a new concept and will meaningfully add to the conceptual load of C#.
  • It's not a cheap feature to build. We should make sure it's worth it.

Alternatives

Structural constraints

An alternative approach would be to have "structural constraints" directly and explicitly requiring the presence of specific operators on a type parameter. The drawbacks of that are:
- This would have to be written out every time. Having a named constraint seems better.
- This is a whole new kind of constraint, whereas the proposed feature utilizes the existing concept of interface constraints.
- It would only work for operators, not (easily) other kinds of static members.

Default implementations

An additional feature to this proposal is to allow static virtual members in interfaces to have default implementations, just as instance virtual members do. We're investigating this, but the semantics get very complicated: default implementations will want to call other static virtual members, but what syntax, semantics and implementation strategies should we use to ensure that those calls can in turn be virtual?

This seems like a further improvement that can be done independently later, if the need and the solutions arise.

Virtual static members in classes

Another additional feature would be to allow static members to be abstract and virtual in classes as well. This runs into similar complicating factors as the default implementations, and again seems like it can be saved for later, if and when the need and the design insights occur.

Unresolved questions

Called out above, but here's a list:

  • Operators == and != as well as the implicit and explicit conversion operators are disallowed in interfaces today. Should they be allowed?
  • Should the qualifying I. in an explicit operator implenentation go before the operator keyword or the operator symbol (e.g. +) itself?
  • Should we relax the operator restrictions further so that the restricted operand can be of any type that derives from, or has one of some set of implicit conversions to the enclosing type?

Design meetings

@MadsTorgersen MadsTorgersen added this to the Working Set milestone Feb 12, 2021
@MadsTorgersen MadsTorgersen self-assigned this Feb 12, 2021
@leandromoh
Copy link
Contributor

leandromoh commented Feb 12, 2021

About Static interface members other than fields are allowed to also have the abstract modifier. Abstract static members are not allowed to have a body (or in the case of properties, the accessors are not allowed to have a body).

I would expect I could define a default implementation for static members, because

  • abstracts classes allow default implementation
  • static interfaces allow default implementation, since C#8
  • in some cases is very convenient to define default implementation, for example if we define an interface to check equality, one can let pending of implementation == static operator and as default implementation for != just negate the return of ==

@CyrusNajmabadi
Copy link
Member

@leandromoh Then you would not mark it 'abstract'. It would be a non-abstract static interface member.

@leandromoh
Copy link
Contributor

@leandromoh Then you would not mark it 'abstract'. It would be a non-abstract static interface member.

I see, great!

@HaloFour
Copy link
Contributor

@leandromoh

I would expect I could define a default implementation for static members

That is mentioned under "Default implementations" and I think describes the concept of virtual static members.

@333fred
Copy link
Member

333fred commented Feb 13, 2021

leandromoh

I would expect I could define a default implementation for static members

That is mentioned under "Default implementations" and I think describes the concept of virtual static members.

Yes, in LDM we discussed being able to put both virtual and abstract on static members. The difference there would be the same as in abstract classes: abstract does not have a body, virtual does.

@alrz
Copy link
Member

alrz commented Feb 13, 2021

My understanding is that this would be only available through constrained generics.

Would we somehow be able to define virtual extension methods?

interface ITokenExtensions {
    abstract static bool IsLiteral(this Token tk);
}
class C<T> where T : ITokenExtensions {
    // ignore the fact that currently using static excludes extensions and type-level usings don't exist
    using static T; 
}

Though I think shapes would be better suited for this case.

shape SToken {
    bool IsLiteral { get; }
}
implement SToken for MyToken {
    bool IsLiteral { get { .. } }
}
interface SToken<TThis> {
    abstract static bool get_IsLiteral(TThis @this);
}
struct SToken_for_MyToken : SToken<MyToken> { 
    public static bool get_IsLiteral(MyToken @this) { .. }
}

And kind of covers "extension everything" as well.

@Trayani
Copy link

Trayani commented Feb 14, 2021

It might be too soon to ask, but in case this feature gets added to the language, would it makes sense to add some general-purpose interfaces-and-their-implementations to BCL (such as IAddable for numeric types?).

@alrz , I believe your example should be indeed covered by shapes which revolve around implicit implementation. static abstract will still require explicit implementation.

@CyrusNajmabadi
Copy link
Member

@Trayani yes. This was discussed as part of the design.

@HaloFour
Copy link
Contributor

@CyrusNajmabadi

yes. [IAddable for numeric types] was discussed as part of the design.

Is it known if the approach being considered to support this by the runtime would be a zero-cost abstraction? I understand that the BCL considered adding numeric interfaces quite some time ago but they ended up being considered unwieldy and to have too much performance overhead so they got axed.

@alrz
Copy link
Member

alrz commented Feb 15, 2021

Is it known if the approach being considered to support this by the runtime would be a zero-cost abstraction?

Couldn't that all be runtime intrinsics? So in practice all of it should compile away at runtime.

Also I think the actual impl would be more involved than that. Looking at rust implementation (https://doc.rust-lang.org/src/core/ops/arith.rs.html) it could turn out to be something like IAddable<TThis, TRhs, TOutput>. you need a few other features to make that less unwieldy still (default constraints, associated types, etc).

@orthoxerox
Copy link

@HaloFour I don't see why it can't be a ZCA for struct types. They get specialized copies of the generic methods, so baking the right implementation into each copy should be straightforward.

@MgSam
Copy link

MgSam commented Feb 17, 2021

Not having virtual/abstract static members in classes ship at the same time will present a weird scenario where you have to move static members to interfaces if you want to abstract over them. It breaks some of the symmetry present between classes/interfaces.

If this feature and default implementations are too costly to design because of static virtual members calling each other, then a reasonable compromise would just be to ban them calling each other in the first version of the feature and revisit in the future if there's interest.

@p-lindberg
Copy link

I came to think of a situation where this feature would be handy, and figured I'd contribute it to the discussion as another reason to consider this feature:

When writing generic methods, you sometimes face the issue that you require some information about the generic type itself (as opposed to an instance of the type), and currently there is no great way to enforce that that information exists. For instance, say you have a generic method that places some type of resource (i.e. a generic type) into a cache, and each type of resource should define a key that it should be cached under.

Some options we have today:

  • Define an interface IResource with the string CacheKey { get; } property. However, then you could only access the key if you have an instance of the resource, so you are constrained to only putting resources into the cache, not taking them out, as then you don't have an instance yet.
  • Define attributes on all resources which contains the cache key. This is suitable for this type of type meta-data, but there is no way to enforce that these attributes exists, and therefore no way to communicate to a client of an API that they need to define the attribute.
  • More dynamic solutions like registering key/type pairs in a dictionary. Not much different from using attributes.
  • Abuse of generics and ad-hoc types: IResource<TKeyProvider> where TKeyProvider : KeyProvider, new() and public abstract class KeyProvider { public abstract string Key { get; } }. In other words, require there to exist a class whose only purpose is to specify a key for each resource.
  • Obviously, in this scenario, a simple out would be to use the type name as the key, but that's beside the point.

If interfaces could have abstract static properties and methods, then we could simply place static abstract string CacheKey { get; } on IResource and treat resources generically even when we don't have an instance available.

I guess my point is that having this feature would allow us to write very nice generic APIs that communicate very clearly to the client how to use them, while at the same time allowing us to write much more concise code that can deal with a broad range of types in a generic way. I therefore think this would be a very valuable addition the language.

I also have a feeling that this has the potential to enable a lot of new powerful meta programming, and that's always fun.

@Thaina
Copy link

Thaina commented Mar 5, 2021

Want to +100 for this if possible

@pakoito
Copy link

pakoito commented Mar 11, 2021

IMonoid

@poke
Copy link
Contributor

poke commented Mar 11, 2021

Just as some clarification since the opening motivational example was mostly based on operators: Will this allow interface declarations like the following?

public interface IAsyncFactory<T>
{
    abstract static Task<T> CreateAsync();
}

public interface IExampleStrategy
{
    abstract static bool IsEnabled(string foo);
    void DoStuff(string foo);
}

@333fred
Copy link
Member

333fred commented Mar 11, 2021

Just as some clarification since the opening motivational example was mostly based on operators: Will this allow interface declarations like the following?

As proposed, yes, you could define those abstract statics. Math may be the motivating example, but factories will also be possible.

@zahirtezcan
Copy link

I do hope these don't result in boxing or virtual-calls. Otherwise using a generic method on an array of values would be a pitfall to avoid for all newcomers.

Also, why not just call it as static interface such as a static class and make it easier instead of all static abstract typing for lazy people like I am :)

@HaloFour
Copy link
Contributor

@zahirtezcan

Also, why not just call it as static interface such as a static class and make it easier instead of all static abstract typing for lazy people like I am :)

A single interface could have both required instance and static members.

@CyrusNajmabadi
Copy link
Member

Also, why not just call it as static interface such as a static class and make it easier instead of all static abstract typing for lazy people like I am :)

A static interface would require all members to still be declared as static, just like static classes do.

@Kant8
Copy link

Kant8 commented Mar 12, 2021

Also, why not just call it as static interface such as a static class and make it easier instead of all static abstract typing for lazy people like I am :)

A static interface would require all members to still be declared as static, just like static classes do.

I'd say this can be a nice addition anyway. Same as for classes, by default interfaces can contain both static and instance methods, but static interfaces can have only static methods.
Maybe it'll be additional hint for compiler to properly get rid of boxing in such cases, when you explicitly notify that you don't need any instance information while using this interface.

@lambdageek
Copy link
Member

lambdageek commented Mar 19, 2021

Something that came up in the runtime discussion dotnet/runtime#49558 makes me wonder about the language proposal:

Will there be a syntax for referring directly to operators? int.operator+ (for example) doesn't seem to work at the moment. So I'm not sure if T.operator+ would work, too.

Example:

public static T SumAll<T> (IEnumerable<T> seq) where T: IAddable<T> {
        return seq.Aggregate(T.Zero, T.operator+);
}

@BreyerW
Copy link

BreyerW commented Mar 19, 2021

@lambdageek Recommended way to implement operators was always that you provide normal method that actually implements operator and call that method in operator. So for your example you would implement operator and static int Add(int,int) and call the latter for aggregate. Not ideal but this workaround is good enough i think

@333fred
Copy link
Member

333fred commented Sep 25, 2022

non-virtual instance members also are public and abstract by default so I don't see your point.

static void M() { } was legal in interfaces in C# 8. It already has meaning.

@CyrusNajmabadi
Copy link
Member

non-virtual instance members also are public and abstract by default so I don't see your point.

We're not talking about those types of members though. We're talking about static members. We already shipped static members with the semantics being that they are not virtual. We can't change that. So that means we needed a way to mark a static member as being virtual, so that required explicit syntax to distinguish the two cases.

@jcouv jcouv modified the milestones: Working Set, 11.0 Sep 26, 2022
@Kimi-Arthur
Copy link

Hi there,

In this line,

M<I<C>>(); // Disallowed: I is an interface

can we lighten the restriction to that as long as the abstract static method is not actually called in it, the case is allowed? I get that this check may not be perfect (that another method called in this method or class may actually use it). But in the simple case of the type (I) is passed elsewhere, it's safe to allow it.

With this, we can ask implementing classes of the interface to have that method, but still allow the original interface to act like a normal interface in other cases.

@333fred
Copy link
Member

333fred commented Nov 17, 2022

@Kimi-Arthur see #5955 for a description of the typing hole doing so would introduce.

@333fred 333fred added the Implemented Needs ECMA Spec This feature has been implemented in C#, but still needs to be merged into the ECMA specification label Jan 9, 2023
@JamesNK JamesNK modified the milestones: 11.0, 10.0 Jan 20, 2023
@TahirAhmadov
Copy link

Has there been any movement on adding static abstract members to abstract class (in addition to interface)?

@Thaina
Copy link

Thaina commented Apr 2, 2023

Has there been any movement on adding static abstract members to abstract class (in addition to interface)?

That seem impossible

abstract member normally safe to assume that they would not be called because abstract class and interface cannot create any instance. That was not the case for static member and so we cannot allow it to be abstract

@timcassell
Copy link

timcassell commented Apr 2, 2023

Has there been any movement on adding static abstract members to abstract class (in addition to interface)?

That seem impossible

abstract member normally safe to assume that they would not be called because abstract class and interface cannot create any instance. That was not the case for static member and so we cannot allow it to be abstract

I think it could be made possible with the introduction of a concrete type constraint.

But I think @TahirAhmadov meant static virtual in an abstract class, no? Static abstract is the title of this issue, after all.

@MCGPPeters
Copy link

With non-static-abstract interface properties it's possible to define default implementations. Has this been considered for static abstracts as well? Here's an example (albeit a bad one, because one that actually showcases the utility would be too involved, I think):

public interface IFoo<T> where T : IFoo<T>
{
    static abstract Func<T, bool> ToBool { get; }
    static abstract Func<T, int> ToInt { get => x => ToBool(x) ? 1 : 0; }
}

I can think of genuine uses for this, where I have some set of features that can be easily implemented in terms of a few operations, but where individual classes that implement the interface may want to override those definitions with ones that take better advantage of their internals to improve performance.

EDIT: Ah, whoops, I see this is described as a "static virtual" at the bottom of the proposal, my bad. I would be happy to see that added, if it's feasible to do so. EDIT 2: (..Apparently it is working in the previews already, nice!)

I see that "static virtual" and providing a default implementation is supported, however I don't get the option to override that default behavior when implementating the interface... Is this this in the works? Or am I missing something?

@CyrusNajmabadi
Copy link
Member

@MCGPPeters

public interface IFoo<T> where T : IFoo<T>
{
    static abstract Func<T, bool> ToBool { get; }
    static virtual Func<T, int> ToInt { get => x => T.ToBool(x) ? 1 : 0; }
}

class C : IFoo<C>
{
    public static Func<C, bool> ToBool => throw new NotImplementedException();

    // > however I don't get the option to override that default behavior when implementating the interface
    // This is the override of hte default behavior.  
    public static Func<C, int> ToInt => throw new NotImplementedException();
}

@MCGPPeters
Copy link

Tnx for your quick reply... Ah.. ok... so no "override" keyword needed ... That is the part that confused me I guess since it differs from overriding virtual instance members.... and the lack of intellisence.

@CyrusNajmabadi
Copy link
Member

@MCGPPeters Right. It's not an override. It's an impl of the interface member (which happens to also have a default impl if no impl was provided). That's never bene considered an 'override' in the DIM world (and statics don't change that). Thanks! :)

@MCGPPeters
Copy link

@CyrusNajmabadi I stand corrected... I guess the point I wanted to make there is that the discoverability of static interface members with a default implementation that can be 'overridden' could be improved (at least for me :) ). When overriding a virtual instance member, IntelliSense helps me by showing which members I can override when typing 'override'. In this case, even though the static member is marked virtual, I don't have that. I would have to read the interface source file to discover this and copy the member definition and replace the generic parameters (in your sample T by C). I might not have that readily available when it is an interface from a library I don't own... Make sense? Or is there a way I am not aware of...

Thanks again

@limeniye
Copy link

I'm waiting for this feature to appear for netstandard.

@KennethHoff
Copy link

That's never happening, @limeniye.
.Net Standard 2.1 is the last version

@tannergooding
Copy link
Member

tannergooding commented Feb 28, 2024

Note the blog that covers this: https://devblogs.microsoft.com/dotnet/the-future-of-net-standard/

.NET Standard 2.1 was the "last" version because moving forward we just have the unified .NET 5, 6, 7, 8, .... So in a way, .NET 5 is itself .NET Standard vNext and so on for each subsequent version.

So the static virtuals in interfaces feature is itself simply .NET 7 or later. Any runtime that implements the .NET 7 surface area is required to provide this support. -- Noting that it being runtime dependent is also why it will never be available on older runtimes, like .NET Framework.

@andy840119

This comment has been minimized.

@jcouv jcouv added the Proposal label Sep 17, 2024
@dotnet dotnet locked as resolved and limited conversation to collaborators Dec 12, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Implemented Needs ECMA Spec This feature has been implemented in C#, but still needs to be merged into the ECMA specification Proposal champion Proposal
Projects
None yet
Development

No branches or pull requests