Search code examples
c#inheritanceinterfaceabstract-classc#-11.0

C# 11 - Static abstract members in interfaces through abstract classes?


C# 11 introduced static abstract members in interfaces.

But from what I'm experimenting, it only force the direct child to implement these static abstract members. That static abstract modifier can't be used in an abstract class.

An example of what I'm trying to do, which doesn't compile:

public interface IMyInterface
{
    public static abstract void DoSomething();
}

public abstract class MyAbstractClass : IMyInterface
{
    public static abstract void DoSomething(); //Error: Overridable method cannot be static
}

public class MyClass : MyAbstractClass
{
    public static override void DoSomething() => Console.Log("Hello, World!");
}

But without an abstract class between them, MyClass does compile just fine:

public class MyClass : IMyInterface
{
    public static void DoSomething() => Console.Log("Hello, World!");
}

Did I miss something? And why would it be the case?


I believe I already know the workaround of my case, but this way does not force every child of MyAbstractClass to implement IMyInterface, just like what we would do with regular interface members.

public interface IMyInterface
{
    public static abstract void DoSomething();
}

public abstract class MyAbstractClass
{
    
}

public class MyClass : MyAbstractClass, IMyInterface
{
    public static void DoSomething() => Console.Log("Hello, World!");
}

Solution

  • But from what I'm experimenting, it only force the direct child to implement these static abstract members. That static abstract modifier can't be used in an abstract class.

    Correct.

    Did I miss something? And why would it be the case?

    • This is by design.
    • ...because there is no reason to introduce static abstract members to a class type.
    • ...because such a member could never be used (outside of a constrained generic context).
    • ...because static members are not invoked via a virtual-call (vtable lookup) because there's no this reference from which to obtain a vtable reference from.
    • ...because it's static (static methods don't have the implicit (and hidden) this parameter).

    By example:

    class SubclassA : IMyInterface
    {
        public static void DoSomething() => Console.WriteLine( "Explosive bolts, ten thousand volts, At a million miles an hour" );
    }
    
    class SubclassAB : SubclassA
    {
        public static void DoSomething() => Console.WriteLine( "Life is short and love is always over in the morning." );
    }
    
    class SubclassB : IMyInterface
    {
        public static void DoSomething() => Console.WriteLine( "Hot metal and methedrine." );
    }
    
    //
    
    public void Main()
    {
        MyMethod( new SubclassA() );
        MyMethod( new SubclassAB() );
        MyMethod( new SubclassB() );
    }
    
    public void MyMethod( IMyInterface instance )
    {
        // At this point, how do you propose invoking `DoSomething`? (without using reflection)
        // ...see the problem?
    }
    

    However, interface types do support static abstract members, but only because they're useful in the context of generic method constraints, as it allows generic code to specify methods for call-sites that are non-virtual: when the JIT/runtime instantiates a generic method it emits static method calls, not vtable-based calls.

    (For Swift users, this is kinda like how protocol types are not equivalent to C# or Java interface types, because a protocol supports non-boxing calls to value-types, whereas (in non-generic methods) C# interface types are always treated as reference-types, but anyway).

    I believe I already know the workaround of my case, but this way does not force every child of MyAbstractClass to implement IMyInterface

    It sounds like you're using interface types as a kind-of linter, to guarantee that a set of class types all follow some kind of common coding-convention and implement some common set of members (in this case, static members), even if you never utilize that interface anywhere in your program - but this is not what interface types in C# are intended for.


    Whereas if you make MyMethod a generic method and use IMyInterface as a constraint then it becomes useful because now you can invoke the static method by using the type-parameter as a type-name:

    public void MyGenericMethod<T>( T instance )
        where T : IMyInterface
    {
        T.DoSomething();
    }
    
    public void Main()
    {
        GenericMethod<SubclassA>(); // Will print "Explosive bolts, ten thousand volts, At a million miles an hour"
        GenericMethod<SubclassAB>(); // <-- This actually behaves the same as SubclassA.
        GenericMethod<SubclassB>(); // Will print "Hot metal and methedrine."
    }
    

    ...though the call with SubclassAB behaves the same as SubclassA, because with SubclassAB the call to T.DoSomething() gets routed to SubclassA.DoSomething() even though SubclassAB.DoSomething() exists.

    ...I don't know why this doesn't work (yet). The original proposal document simply puts "TBD" for this, and oddly I can't find any relevant bugs filed in the Roslyn repo, so I'll update this answer when I find out.


    I suppose you can argue that there's no difference between a T : BaseClass constraint and a T : IInterface constraint - at least insofar as this code (below) arguably should be allowed to work, but it doesn't (CS0704)

    public class BaseClass
    {
        public static void DoSomething() => Console.WriteLine( "Strange men rent strange flowers" );
    }
    
    public class DerivedClass : BaseClass
    {
        public static void DoSomething() => Console.WriteLine( "Mundane by day inane at night" );
    }
    
    public static void Main()
    {
        GenericMethod<AbstractClass>(); // Should print "Strange men rent strange flowers"
        GenericMethod<DerivedClass >(); // Should print "Mundane by day inane at night"
    }
    
    public static void GenericMethod<T>()
        where T : BaseClass
    {
        Type t = typeof(T);
        T.DoSomething(); // CS0704
    }