Search code examples
c#static-methodsabstractc#-11.0default-interface-member

Is it possible to call a static abstract interface method from implementation of another static interface method?


The question is simply in the comment of code below By the way this code is valid only with C#-11 language version (ie: create a Net 8.0 framework project or later to test).

Is the problem of code below a limitation of new C# syntax (which would be sad because this use case is, I guess, one of the most wanted) ? Or am i wrong on syntax ?

My aim is to use later "IDynamicEnum" on a type known only at runtime (by using reflection). But i am not sure if static interface method is meant to be used s i try to do here or if it is only for use case where T is always known at compile time. i am just playing with C# 11

I know I could remove interface IDynamicEnum completely and use something like typeof(IDynamicEnum<>).MakeGenericType(the_type_is_got_by_reflection) but this question seems interesting anyway.

    public interface IDynamicEnum
    {
        string Name { get; }
        ulong Value { get; }

        static abstract IEnumerable<object> GetAllValues();
    }

    /// <inheritdoc cref="IDynamicEnum"/>
    public interface IDynamicEnum<out T> : IDynamicEnum
        where T : class, IDynamicEnum<T>
    {
        // A better signature: We strongly type
        new static abstract IEnumerable<T> GetAllValues();

        // I would like to automatically implement the non generic method...
        // To do that, intuitively i would like to write this line 
        // which is kind of a "return type covariance on static interface method"
        // But sadly it gives error CS8926
        //static IEnumerable<object> IDynamicEnum.GetAllValues() => GetAllValues();
    }


    public class MyDynamicEnum : IDynamicEnum<MyDynamicEnum>
    {
        public string Name { get; }
        public ulong Value { get; }

        public static MyDynamicEnum Foo { get; } = new MyDynamicEnum("Foo", 1);
        public static MyDynamicEnum Bar { get; } = new MyDynamicEnum("Bar", 2);
        public static MyDynamicEnum Baz { get; } = new MyDynamicEnum("Baz", 3);

        public static IEnumerable<MyDynamicEnum> AllValues { get; } = new[] { Foo, Bar, Baz };

        protected MyDynamicEnum(string name, long value) { Name = name; Value = value; }

        public static IEnumerable<MyDynamicEnum> GetAllValues() => AllValues;

        // So I could get rid of this annoying / duplicated line
        static IEnumerable<object> IDynamicEnum.GetAllValues() => AllValues;
    }

EDIT: continue reading only if the abstract question above is not enough for you

The real application is for kind of enum not know in advance, for example to handle all the tiny set of values related to parsing csproj/sln files:

  • Configuration ("Debug", "Release", custom ones defined by users... no way to know them in advance)
  • Platforms ("x86", x64", "Any Cpu", the later as a lot of different writings, with space, with "CPU" or "cpu")
  • Framework: Contains a lot of different values. The standard ones (which i want to handle): "net48", ..., "net8.0", but also a lot of fancy/rare value i don't want to handle, for example "net7.0-tvos". They also have a lot writing possible (with a dot or not...)

So to have "proper" algorithms it is better to have a way to identify them simply without handling all of their writting form everytime. This is more a Flyweigh pattern than a dynamic enum concept actually. Anyway if user wants to expand the DynamicEnum type to add the support of "net7.0-android" and create the static singleton instance in its code to reference it by his algorithm, my code should not prevent him to do that

So.. ok the name "DynamicEnum" is maybe not right. but i wanted to have a simple question, not enter in a debate about XY problem.

Morever I want this code to work both in net48 and net8.0, So i take advantage of compiler because it will check the code compiles for bioth net8.0 and net4.8. So the static method constraint give me more proability the method also exist for net48 (no guarantee i know, but more than if a expect developper to read the xml doc...)

My current solution looks like this:

    // comment for stackoverflow: this interface looks simple / intuitive
    public interface IDynamicEnum
    {
        /// <summary> Represent the culture invariant technical representation name </summary>
        string NormalizedName { get; }

        /// <summary> The value of the enum (if supported, see MaxValueRepresentation) </summary>
        ulong Value { get; }
    }

    // comment for stackoverflow: This interface adds "behavior" allowed by new C# 11 syntax
    public interface IDynamicEnum<out T> : IDynamicEnum
        where T : class, IDynamicEnum<T>
    {
#if NET8_0_OR_GREATER

        static abstract ulong? MaxValueRepresentation { get; }
        static abstract IEnumerable<T> GetAllValues();

        // Is it not possible to write something like this (that does not cause a CS8926 error)  ?
        //static IEnumerable<IDynamicEnum> IDynamicEnum.GetAllValues() => GetAllValues();
#endif

    }

    // comment for stackoverflow: this interface is the helper class that works both for net48/net8.0
    public static class IDynamicEnum_Extensions
    {
#if NET8_0_OR_GREATER
        public static IEnumerable<T> GetAllValues<T>()
            where T: class, IDynamicEnum<T>
        {
            return T.GetAllValues();
        }
#else
        public static IEnumerable<T> GetAllValues<T>()
            where T : class, IDynamicEnum<T>
            => (IEnumerable<T>)_GetAllValues(typeof(T));
#endif

        public static IEnumerable<IDynamicEnum> GetAllValues(Type dynamicEnumType)
        {
            var interfaceType = typeof(IDynamicEnum<>).MakeGenericType(dynamicEnumType);
            if (!interfaceType.IsAssignableFrom(dynamicEnumType))
                throw new ArgumentException($"Type {dynamicEnumType} is not implementing {interfaceType}", nameof(dynamicEnumType));
            return (IEnumerable < IDynamicEnum > )_GetAllValues(dynamicEnumType);
        }
        static IEnumerable<object> _GetAllValues(Type type)
        {
            var m = type.GetMethod("GetAllValues", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
            var enumerable = m.Invoke(null, null);
            return (IEnumerable<object>)enumerable;
        }
    }



// comment for stackoverflow: Now a class implementing it, note there is no more "#if NET80..." line here:

    [DebuggerDisplay("{" + nameof(DisplayName) + ",nq}")]
    public class eConfig : IDynamicEnum<eConfig>
    {
        public static eConfig Debug { get; } = new eConfig("Debug");
        public static eConfig Release { get; } = new eConfig("Release");
        public string DisplayName { get; }

        /// <summary> Name of platform lowercased no duplicate space, trimmed </summary>
        public string NormalizedName { get; }

        /// <summary>
        /// Open constructor to add custom / weird config as static public property in a child class.
        /// </summary>
        protected eConfig(string displayName, string normalizedName = null)
        {
            DisplayName = displayName;
            NormalizedName = Normalize(normalizedName ?? displayName);
            _AllStandardConfigurations = _AllStandardConfigurations ?? new();
            _byNormalizedNames = _byNormalizedNames ?? new();

            if (null == TryGetByNormalizedName(NormalizedName))
            {
                _byNormalizedNames.Add(NormalizedName, this);
                _AllStandardConfigurations.Add(this);
            }
        }

        public override string ToString()
        {
            return DisplayName;
        }

        public static IReadOnlyCollection<eConfig> AllStandardConfigurations => _AllStandardConfigurations;
        static List<eConfig> _AllStandardConfigurations;
        static Dictionary<string, eConfig> _byNormalizedNames;

        public static eConfig GetByNormalizedName(string name)
        {
            return TryGetByNormalizedName(name) 
                ?? throw new TechnicalException($"{name} not recognized as a valid configuration (or not yet handled, use constructor for that!)");
        }
        public static eConfig TryGetByNormalizedName(string name)
        {
            if (_byNormalizedNames.TryGetValue(Normalize(name), out var result))
                return result;
            return null;
        }

        static string Normalize(string name)
        {
            while (name.Contains("  "))
                name = name.Replace("  ", " ");
            return name.Trim().ToLowerInvariant();
        }


        public static IEnumerable<eConfig> GetAllValues() => _byNormalizedNames.Values;
        public static ulong? MaxValueRepresentation => null;
        string IDynamicEnum.NormalizedName => NormalizedName;
        ulong IDynamicEnum.Value => throw new NotSupportedException();
    }

Solution

  • It will work with using the T type parameter - T.GetAllValues() as the error suggests. This is as long as we have the generic constraint where T: I<T>. There is a section in the documentation which advocates using this pattern with virtual static methods.

    But why do we need T so badly to make a simple call to another method in the same interface? Consider the following simplified example:

    public interface I {
        static virtual object GetValue() {
            return "interface implementation";
        }
        
        static object DefaultValue() => new object();
        
        static virtual object Hello(){
            return I.DefaultValue();
            //return I.GetValue(); // CS8926 
        }
    }
    

    It will compile fine if we just returned a call to the pure static method I.DefaultValue(). But it won't if we made a call to the virtual static method GetValue - giving us the same error from your initial example:

    CS8926 A static virtual or abstract interface member can be accessed only on a type parameter.
    

    End of the day, the C# compiler has to emit IL code. What IL code will enable static virtual dispatch? callvirt would necessitate an object we don't have here in the body of a static method. You can use call but then you are making a direct method call - but to which method?

    That's why they expanded the constrained prefix to handle this polymorphism requirement:

    Polymorphic behavior of calls to these methods is facilitated by the constrained. call IL instruction where the constrained. prefix specifies the type to use for lookup of the static interface method.

    This is how it would look like:

    IL_0001: constrained. !!T /* 1B000005 */
    IL_0007: call object I::GetValue() /* 06000013 */
    

    more from the ECMA addendum on how the static dispatch works with the help of the constrained:

    The behavior of the constrained. prefix is to change the method that the call or ldftn instruction refers to be the method on implementorType which implements the virtual static method

    Static methods are methods that are associated with a type, not with its instances. For static virtual methods, the particular method to call is determined via a type lookup based on the constrained. IL instruction prefix or on generic type constraints but the call itself doesn't involve any instance or this pointer.

    So, the C# compiler emits the type information with the constrained prefix that would be needed downstream by the JIT compiler for the type lookup in determining which exact method to call. Information that would otherwise the JIT compiler might not be able to get in any other way to facilitate the virtual dispatch (no this pointer).