C# 8 base interface's default method invocatio

2020-07-10 02:49发布

问题:

According to https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-8.0/default-interface-methods It is possible to explicitly invoke an interface base implementation with the following syntax.

base(IInterfaceType).Method();

But this doesn't seem to be implemented yet.

Is there a workaround (e.g reflection) to achieve this?


Example code to illustrate the problem

interface IA
{
    void M()
    {
        Console.WriteLine("IA.M");
    }
}

interface IB : IA
{
    void IA.M()
    {
        Console.WriteLine("IB.M");
    }
}

interface IC : IA
{
    void IA.M()
    {
        Console.WriteLine("IC.M");
    }
}

class D : IA, IB, IC
{
    public void M()
    {
        // base(IB).M(); Is not yet supported apparently
        ((IB)this).M(); // Throws stack overflow
    }
}

class Program
{
    static void Main(string[] args)
    {
        D d = new D();
        d.M();
    }
}

回答1:

The link in the question points to a version of the proposal copied from the proposal document in Github

The feature was cut in April 2019

Conclusion

Cut base() syntax for C# 8. We intend to bring this back in the next major release.

The design meeting doc explains that without runtime support (which wouldn't be available in time), the implementation would be workable at best for C# but not VB.NET.

If B.M is not present at run time, A.M() will be called. For base() and interfaces, this is not supported by the runtime, so the call will throw an exception instead. We'd like to add support for this in the runtime, but it is too expensive to make this release.

We have some workarounds, but they do not have the behavior we want, and are not the preferred codegen. Our implementation for C# is somewhat workable, although not exactly what we would like, but the VB implementation would be much more difficult. Moreover, the implementation for VB would require the interface implementation methods to be public API surface.

As for the infinite recursion, this

public void M()
{
    ((IB)this).M(); // Throws stack overflow
}

That's essentially

public void M()
{
    M(); // Throws stack overflow
}

Default interface members are called the same way explicitly implemented interface methods are, through the interface. Besides, you're asking to call the method on this, not base.



回答2:

There is a workaround. I got it working, using GetFunctionPointer

Warning do not use this code

static class BaseInterfaceInvocationExtension
{
    private static readonly string invalidExpressionMessage = "Invalid expression.";

    public static void Base<TInterface>(this TInterface owner, Expression<Action<TInterface>> selector)
    {
        if (selector.Body is MethodCallExpression methodCallExpression)
        {
            MethodInfo methodInfo = methodCallExpression.Method;
            string name = methodInfo.DeclaringType.FullName + "." + methodInfo.Name;
            Type type = owner.GetType();
            InterfaceMapping interfaceMapping = type.GetInterfaceMap(typeof(TInterface));
            var map = interfaceMapping;
            var interfaceMethod = map.InterfaceMethods.First(info =>
                info.Name == name);
            var functionPointer = interfaceMethod.MethodHandle.GetFunctionPointer();

            var x = methodCallExpression.Arguments.Select(expression =>
            {
                if (expression is ConstantExpression constantExpression)
                {
                    return constantExpression.Value;
                }
                var lambda = Expression.Lambda(Expression.Convert(expression, expression.Type));
                return lambda.Compile().DynamicInvoke();
            }).ToArray();
            Type actionType = null;
            if (x.Length == 0)
            {
                actionType = typeof(Action);
            }else if (x.Length == 1)
            {
                actionType = typeof(Action<>);
            }
            else if (x.Length == 2)
            {
                actionType = typeof(Action<,>);
            }
            var genericType = actionType.MakeGenericType(methodInfo.GetParameters().Select(t => t.ParameterType).ToArray());
            var instance = Activator.CreateInstance(genericType, owner, functionPointer);
            instance.GetType().GetMethod("Invoke").Invoke(instance, x);
        }
        else
        {
            throw new Exception(invalidExpressionMessage);
        }
    }
}

class D : IA, IB, IC
{
    public void M(int test)
    {
       this.Base<IB>(d => d.M(test));
    }
}

class Program
{
    static void Main(string[] args)
    {
        D d = new D();
        d.M(12);
        Console.ReadKey();
    }
}


回答3:

This is a workaround. It isn't ideal. Perhaps it will help someone.

class C : IB
{
    public void IBM() => (this as IB).M();
}

class D : IA, IB, IC
{
    private C _c = new C();

    public void M()
    {
        _c.IBM();
    }
}