dotnet / csharplang

The official repo for the design of the C# programming language
11.23k stars 1.02k forks source link

[Open issue]: static abstract interfaces and static classes #5783

Open stephentoub opened 2 years ago

stephentoub commented 2 years ago

With static abstract interface methods, consider:

using System;

public class C : IMethods
{
    public static void M() { }
}

internal interface IMethods
{
    static abstract void M();
}

This compiles fine. However, if I try to make C be a static class:

using System;

public static class C : IMethods
{
    public static void M() { }
}

internal interface IMethods
{
    static abstract void M();
}

it fails to compile with:

error CS0714: 'C': static classes cannot implement interfaces

Such an error arguably made sense before static abstract interface methods, as interface methods could only be instance members, and static classes can't have instance members (and even for an empty interface, you can't create instances of a static class and thus couldn't even use the interface as a typical marker used with is casts).

However, with static abstract interface methods, you can have an interface entirely composed of static abstract members, and it would logically make sense to have a static class implement that interface.

We could address this by either:

  1. Simply removing the error and allowing static classes to implement interfaces. If you try to implement one that has instance members, you won't be able to given the class is static, and you'll get errors about not fully implementing the interface.
  2. Allowing interfaces to be marked as static interface such that it can only contain static abstract members and not instance members, and then allow static classes to implement static interfaces.

One of the reasons one might want a static class implementing a static interface is to be able to then use that static class as a generic type argument. However, that is also prohibited by the language today, e.g. this:

using System;

public static class C
{
    public static void M() { }
}

class D
{
    public static void M1<T>(){}

    public static void M2() => M1<C>();
}

results in:

error CS0718: 'C': static types cannot be used as type arguments

We should also consider lifting this constraint.

If we do lift it, then it becomes possible for a generic type or method to create locals, fields, etc. of that generic type, which could be a static class. Those members or locals would then be useless, since you wouldn't have a way to create an instance of them nor invoke anything off of them. If that's something we want to avoid, we could go with option (2) above, allow static classes to be used as a generic type argument when that argument is constrained to a static interface, and then language rules around static types (extended from classes to interfaces) should naturally prevent creating such members and locals.

Design Meetings

https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-16.md#static-abstract-interfaces-and-static-classes

CyrusNajmabadi commented 2 years ago

Simply removing the error and allowing static classes to implement interfaces. If you try to implement one that has instance members, you won't be able to given the class is static, and you'll get errors about not fully implementing the interface.

I like this with one caveat. If the interface has no static members, you still get an error. So this would include an empty interface, as well as an interface with only instance-DIM methods.

Alternatively, we could say: interface needs to be non-empty and only contain static members to be implementable by a static class.

stephentoub commented 2 years ago

That seems reasonable.

siegfriedpammer commented 2 years ago

Alternatively, we could say: interface needs to be non-empty and only contain static members to be implementable by a static class.

Could even be a requirement to add the static modifier to the interface declaration?

stephentoub commented 2 years ago

Could even be a requirement to add the static modifier to the interface declaration?

I believe that's the option (2) I outlined 😄

333fred commented 2 years ago

For option 2, it seems like an unnecessary bifurcation to me, unless regular types can also implement so-called static interfaces.

stephentoub commented 2 years ago

unless regular types can also implement so-called static interfaces

Yes, they'd be able to. You'd just only be able to implement an interface on a static class if the interface was also static, but making the interface static wouldn't require that all implementors be static.

333fred commented 2 years ago

unless regular types can also implement so-called static interfaces

Yes, they'd be able to. You'd just only be able to implement an interface on a static class if the interface was also static, but making the interface static wouldn't require that all implementors be static.

I see. In that case, my gut reaction would lean towards option 2 for the intentionality of the design: otherwise, we introduce another way that you can silently break your users :sweat_smile:

reflectronic commented 2 years ago

Considering that static classes cannot be used as generic type arguments today, how is this feature useful? Without the ability to do that, you cannot actually use an interface as an abstraction for a static class. This feature alone would not really get you anywhere.

CryoMyst commented 2 years ago

2 does seem like the better option here, allowing static interface which restricts instance implementations and restrict from generic type arguments. Whilst reflectronic does make a good point where you can't actually use it for abstractions it is useful sometimes to enforce some sort implementations on a collection of static classes.

stephentoub commented 2 years ago

Considering that static classes cannot be used as generic type arguments today, how is this feature useful? Without the ability to do that, you cannot actually use an interface as an abstraction for a static class. This feature alone would not really get you anywhere.

I don't understand the point. Consider: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Security/src/System/Net/Security/ReadWriteAdapter.cs#L11 which below it is then implemented by two types that only provide static members. Those types are then used as generic type arguments in order to parameterize what various static calls do, e.g. https://github.com/dotnet/runtime/blob/e8fb8084e4667a0d95f1f41270617c09843df5fc/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.Implementation.cs#L756 Right now those types are structs, but they can just as easily be classes, and as classes I'd want them to be static to get compiler validation that I don't accidentally add instance members to them, which is the whole point of being able to put static on classes.

(I'd also like the option of being able to put static on structs to get the same validation, but that's introducing a new concept and is a separate discussion.)

naine commented 2 years ago

This feature does make sense to have along with static abstracts in interfaces, but I agree that as is, the feature alone has no utility.

Those types are then used as generic type arguments in order to parameterize what various static calls do

If the implementing types were static, as this feature would enable, you would no longer be able to use them as generic type arguments, due to error CS0718.

To do this with a static class, the language would also have to be modified to permit static types to be passed as generic type arguments, most likely along with a new type of constraint to indicate what type parameters may be static and prevent the generic code from declaring a variable, field, or parameter of these type arguments.

stephentoub commented 2 years ago

the language would also have to be modified to permit static types to be passed as generic type arguments,

Yes

most likely along with a new type of constraint to indicate what type parameters may be static and prevent the generic code from declaring a variable, field, or parameter of these type arguments

Maybe. To me the utility of a static class isn't that it prevents me from declaring a useless instance (there are many other ways to achieve that), it's that as the creator of the type it helps me avoid problems by adding members to a type that shouldn't be there.

But if we went with option 2, then the language could permit static classes as generic arguments if the generic parameter was constrained to a static interface, and it could also ensure that variables, fields, etc. of such statics weren't created.

TahirAhmadov commented 2 years ago

I would vote for option 2. I think the intention part of it is important just for readability, but also for future maintenance - if somebody tries adding an instance member to that static interface, they are stopped from doing so immediately, and while yes they can just remove the static keyword, at least it's something that they have to do manually - there was a set of eyes who makes that decision. On the other hand, with option 1, an instance member is added, this project is built, then another project which relies on this project fails downstream all of a sudden. Also, at runtime, with option 2, the binder can run a simple check and produce a concise error - interface not static - as opposed to option 1, where the binder has to inspect the surface area and produce a complicated error - interface implemented by static class now contains instance members. Also, it's just one keyword to add on top of an interface, so it's not a crazy amount of typing in the big picture.

siegfriedpammer commented 2 years ago

Could even be a requirement to add the static modifier to the interface declaration?

I believe that's the option (2) I outlined 😄

Well... that's what I get for reading these proposals on my phone... my bad...

Joe4evr commented 2 years ago

You'd just only be able to implement an interface on a static class if the interface was also static, but making the interface static wouldn't require that all implementers be static.

🍝 Would it be plausible to also allow ref structs to implement such a static interface? If the definition of a static interface disallows callers to create instances of its implementers anyway, then I think the dangers of boxing a ref struct instance through said interface no longer apply.

I admit I haven't thought up a use-case for it, it's just something that came to mind.

FaustVX commented 2 years ago
using System;

public static class C
{
    public static void M() { }
}

class D
{
    public static void M1<T>(){}

    public static void M2() => M1<C>();
}

results in:

error CS0718: 'C': static types cannot be used as type arguments

In your example, what the generic constrain could be ? May be something like that :

public static void M1<T>() where T : static {}

But with that kind of constrain, we should still be able to use normal classes.

stephentoub commented 2 years ago

In your example, what the generic constrain could be ?

My example would be augmented to instead be like:

using System;

static class C : I
{
    public static void M() { }
}

static interface I
{
    static void M();
}

class D
{
    public static void M1<T>() where T : I {}

    public static void M2() => M1<C>();
}
FaustVX commented 2 years ago

Ok, so interfaces are needed to do generic with static classes ?

jnm2 commented 2 years ago

Allowing static classes to implement interfaces which only have static members, and to be passed as generic type arguments, was first discussed at https://github.com/dotnet/csharplang/issues/4436#issuecomment-946729152. There's also discussion on just the generic type argument part at https://github.com/dotnet/csharplang/discussions/4840 and https://github.com/dotnet/csharplang/discussions/5517.

Allowing interfaces to be marked static with the effect of requiring all its members to be static was first discussed at https://github.com/dotnet/csharplang/issues/4436#issuecomment-797151352

Richiban commented 2 years ago

I like the ability to declare static interfaces from Option(2), but I'm not sure I would go with the limitation that static classes can only implement static interfaces.

I can totally see a situation where a library has published an interface with all static members (probably a single member, which is static) but the author hasn't marked the interface as static. You've got a static class that you want to pass as a type argument to some method so you need implement said interface but you can't; you're completely SOL.

jnm2 commented 2 years ago

@Richiban Unless you make the static class non-static. If I understand right, that's the price we'd pay in order to avoid adding this new way that it would be a breaking change to add an instance member to an interface (DIM or not).

jeffhandley commented 2 years ago

@MadsTorgersen / @stephentoub / @tannergooding -- Is there anything remaining open on this, or can this issue be closed out?

tannergooding commented 2 years ago

This is an issue tracked by the C# LDM team, so they're responsible for tracking it and closing it.

The support requested by the feature doesn't currently exist but is championed so it likely needs to stay open.

333fred commented 2 years ago

We don't close issues until they are implemented in the ECMA spec.

zvrba commented 1 year ago

The following (somewhat paradoxically) does not work in net6 with preview features enabled:

interface IStatics {
    public abstract static int AI { get; }
    public static int CI => 14;
}

static int M<T>() where T : IStatics {
    return T.AI + T.CI;  // ERROR
    return T.AI + IStatics.CI;  // WORKS
}

The compiler complains on T.CI. The second return statement compiles fine. I see no reason why the first variant should not be allowed, so this is perhaps an oversight in the spec?

Yes, the workaround is simple, but it'd be nice to be able to write the body of M only in terms of its generic parameter instead of switching between T and IStatics depending on whether the method is abstract or not. Not to mention that, if it worked, it'd be possible to change CI to abstract at a later point without changing the implementation of M.

EDIT: Re possible ambiguity if T has its own static definition of CI: use definition from the interface (most intuitive, at least for me, as T : IStatics is the only thing known about T; perhaps a warning should be issued) or make it an error (also intuitive; if overriding is desired, the member should be declared as abstract in the interface). Though I don't know how to handle M being instantiated through reflection in this case. The first behavior (use definition from the interface) is probably the only feasible one.

En3Tho commented 1 year ago

In your example CI is not abstract, it's a legit static member of that interface. You have to make this thing static virtual to get it working from T.

zvrba commented 1 year ago

In your example CI is not abstract, it's a legit static member of that interface. You have to make this thing static virtual to get it working from T.

This does not answer the original question: why the compiler can't (or won't) look up and bind to CI through T when T is declared as T : IStatics To me, this is a deficiency/oversight in design of the feature.

En3Tho commented 1 year ago

Because CI is a direct static member of that interface, it's not a part of interface contract.

tannergooding commented 1 year ago

It’s worth noting that static members are “inherited” and accessible from the derived type in other cases.

System.Runtime.Intrinsics relies on this to help model the “dependency hierarchy” between the various ISAs exposed. Thus, Sse2 inherits from Sse, and each only exposes static members. But Sse2.Sqrt(Vector128<float>) is still valid even though Sse is what defines/exposes that static method.

It’s, IMO, reasonable to expect this very similar situation where T is known to derive from the interface to therefore have access to static members defined by the interface (and for it to ultimately give the IDE hint that it can be simplified down to directly accessing the member form the interface, just as it does for the other described above)

c0nd3v commented 1 year ago

@stephentoub I think this should be finalized for .NET 7/C# 11😃

c0nd3v commented 1 year ago

#63548 which is part of the .NET 7.0 milestone is closed and considered resolved. It marks this issue as complete, but I don't think it is😃

Also the LDM mentions this issue as being included in the working set

stephentoub commented 1 year ago

I think this should be finalized for .NET 7/C# 11

This was not addressed for C# 11.

c0nd3v commented 1 year ago

I think this should be finalized for .NET 7/C# 11

This was not addressed for C# 11.

I see that you edited #63548, that makes more sense now😃 Thanks

I think this should be included in C# 11 for static abstract methods in interfaces to be truly feature complete.

mikernet commented 1 year ago

On a related note: I'm using effectively-static structs to implement static member-only interfaces so that the JIT outputs a specialized value type generic method compilation with devirtualized static interface calls instead of a shared reference type compilation with slower virtualized static interface calls.

It would be nice to be able to declare those structs as static struct.

HaloFour commented 1 year ago

@mikernet

IMO a static struct doesn't make a lot of sense. I understand that you want to use structs here specifically for generic specialization, which is an implementation detail and one that the runtime could provide for effectively static classes as well.

mikernet commented 1 year ago

@HaloFour If runtime support for generic specialization for static classes is on the table then I would gladly take that as an alternative.

hez2010 commented 1 year ago

Another use case is that with static abstract/virtual interfaces, we can implement interfaces for ref structs.

hez2010 commented 1 year ago

It turns out that the .NET runtime already supports implementing interface static members for a ref struct without any problem:

https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AbEBLDaAuIUArgHYA+AAkgAQBiEENA3gL4CwAUFwHRgYBDAM5CaAB2LAM2MDSEwAjsRil82ARhoDSQ7HJgaYAExrAYAM2gXsMDEeyls+eoy4BIGAnwqjogNoAygCeQt4AtjwASmRqYTAAusGhMBEAahrKACpBYjDu2GFiGCkq+KIAkgwQXMzufMShEGE0DqHaYDA0AG4Q2CaBIeFRMQUJSUPRqqM8AMJNYrgwUAFLXTIwQjzlQgBCQZEWADLYANYwAIL4+FDYwMTeICB8+NAAFACUNAC8NK/ubgAGACMNABANB4LB7nedTADRezVa+HanR6fRoA2SEUmsTGgxSPAA8sAhBBit5LtdbvcYI9nm9QjdSABzNCmRgYT4/P6cNyAkFgmgAVgATMKUDQAOwATil4KQQqlAGYaCLwZLJTQkLLJRKkAAOVXyxVIIz/JBihVavV6k0StVSy0m2hWyVOrWdJAqhX/SUqh1IEFurWKgOe8xS+3qlWSxWS9XyiNu32261GrWypCdB26rWG7XK9OSl1xsV+82ZiNZ9NIKu0HOVqWlmveqsmeMVrWyV1ikWdYEQiHQ2HwpotHTI0gdbq9frjAk40aJfHYkZxWbzRbLVbrTZzQpbugGfDEWAHJTYWBGSk3O4PJ5gF5QV6MhzMrm/f4DwUAgTC91ILQQoxhKwaxlqIGFj+ALDtwvI8GIAhgCcoJ1LoABenRAlw7iULKbRqLI/xoiYmQbM4Hw0JQKruLUvJuDwEBdEsNxGJ0cT4AAFhAJgkTQlSMI8ZGhB8/yUIq+o4fRdhgDw2AoDw2H0WAGiaDQ6nqXxmJDHMOhkniWIbnpxSPAA6jc3jHKQMCvA4+BKiKML0bA+DuBwnDubw/DCKIdlLOYSGdBIUgyFo9xMNouhaCS1xIc4AnVJwdFuHh0WMnF3SXieGhyMihE0MRs40MJFGfNRLA0O5nmcHwggiOINxdAI3hhS8Wg6HoZiWLA5g2HYDhODQAAKUAQMyUACGE7ieN4pC+Bi86rlMcTLoZxIAFYwI+NR1Bx3EmGIjXNZ0nF9DAwAhNgzK5c1oWFeiACyAgOJ+9FuK+LJ+PEWhQMyQj/GVNG8slKUSWJKBSXyKkYGpGkzuiVRCeRonOTArm8lVQA=

333fred commented 1 year ago

If you try to use that ref struct in any way that would actually use the static abstract nature of IFoo in that example, it will crash.

August-Alm commented 1 year ago

Just a note: The following already runs and compiles just fine in F# (net7.0):

[<Interface>]
type IM<'t> =
  static abstract f : 't -> string

[<AbstractClass; Sealed>] // abstract + sealed = static in CIL!
type M =
  interface IM<int> with
    static member f n = n.ToString ()

let test<'M, 't when 'M :> IM<'t>> (x : 't) = 'M.f x

printfn "%s" (test<M, int> 5)
m-gallesio commented 1 year ago

I ran into this today. In my use case I would have both static and non-static classes implementing an interface with only static methods.

aradalvand commented 1 year ago

Any chance this makes it to C# 12?

333fred commented 1 year ago

Extremely unlikely.

brantburnett commented 7 months ago

I really like this proposal, but I'd like to add one additional thought around option 2.

Since a static interface can't contain instance members, logically it is little more than just a list methods required to be implemented. Other than clear "documentation" on an implementing class, it's primarily useful within generics. You wouldn't be able to typecast to it like you can a traditional interface.

Therefore, wouldn't it be possible for the C# implementation to be shape-based, like TypeScript interfaces rather than traditional C# interfaces. This would allow any class that implements all of the static methods to be considered as matching a generic constraint on the interface, not just classes that explicitly implement the interface. This could be useful in cases where the static method signatures are matched by a 3rd party library not under the developer's control.

ds5678 commented 6 months ago

If anti-constraints like allow T : ref struct are being introduced into the language, could there be resources to concurrently do allow T : static?

jnm2 commented 6 months ago

@ds5678 I think I'd rather just allow static types to be passed as type parameters everywhere. We already have a very similar situation:

public abstract class BasicallyStatic
{
    private BasicallyStatic() { }
}

new List<BasicallyStatic>() // Where's the danger in this?

To the runtime, there aren't static types, only abstract sealed types.

jnm2 commented 6 months ago

If a static class can implement an interface, it seems interesting to allow a static class to inherit another static class.

static class Shape : IShape { ... }
static class Circle : Shape, ICircle { ... }
ds5678 commented 6 months ago

I think I'd rather just allow static types to be passed as type parameters everywhere.

@jnm2 It does have inherent flexibility and brevity. Those are large benefits that cannot be ignored.

However, it changes the mental model for what a static class is.

ActuallyStatic? local = null;
List<ActuallyStatic?> list = [ local ];

Previously, locals and expressions could not be typed as a static class because such classes could never have instances. Now, we would have to choose between making these methods uncallable or allowing static classes to be a valid target type.

Using an anti-constraint would indicate intent. The compiler could issue errors when a developer attempts to use the generic type argument in an invalid way.

ds5678 commented 6 months ago

If a static class can implement an interface, it seems interesting to allow a static class to inherit another static class.

Although intriguing, I don't think this should be done because changing a static class to an an instance class is not currently a breaking change.

HaloFour commented 6 months ago

Previously, locals and expressions could not be typed as a static class because such classes could never have instances.

Would that need to change? You can coax the runtime to create a List<ActuallyStatic> or ActuallyStatic anyway. Heck, you can force it to create instances through reflection as well. The compiler can keep it's somewhat loose guardrails, while also removing this one limitation.

My concern is that by adding static interfaces that can only be implemented by static classes and can only be used as generic parameters with a static anti-constraint is that it creates needless bifurcation, which could lead to unnecessary friction.