• 【抬杠C#】如何实现接口的base调用


    背景

    在三年前发布的C#8.0中有一项重要的改进叫做接口默认实现,从此以后,接口中定义的方法可以包含方法体了,即默认实现。

    不过对于接口的默认实现,其实现类或者子接口在重写这个方法的时候不能对其进行base调用,就像子类重写方法是可以进行base.Method()那样。例如:

    public interface IService
    {
        void Proccess()
        {
            Console.WriteLine("Proccessing");
        }
    }
    
    public class Service : IService
    {
        public void Proccess()
        {
            Console.WriteLine("Before Proccess");
            base(IService).Proccess(); // 目前不支持,也是本文需要探讨的部分
            Console.WriteLine("End Proccess");
        }
    }

    当初C#团队将这个特性列为了下一步的计划(点此查看细节),然而三年过去了依然没有被提上日程。这个特性的缺失无疑是一种很大的限制,有时候我们确实需要接口的base调用来实现某些需求。本文将介绍两种方法来实现它。

     

    方法1:使用反射找到接口实现并进行调用

    这种方法的核心思想是,使用反射找到你需要调用的接口实现的MethodInfo,然后构建DynamicMethod使用OpCodes.Call去调用它即可。

    首先我们定义方法签名用来表示接口方法的base调用。

    public static void Base<TInterface>(this TInterface instance, Expression<Action<TInterface>> selector);
    public static TReturn Base<TInterface, TReturn>(this TInterface instance, Expression<Func<TInterface, TReturn>> selector);

    所以上一节的例子就可以改写成:

    public class Service : IService
    {
        public void Proccess()
        {
            Console.WriteLine("Before Proccess");
            this.Base<IService>(m => m.Proccess());
            Console.WriteLine("End Proccess");
        }
    }

    于是接下来,我们就需要根据lambda表达式找到其对应的接口实现,然后调用即可。

     

    第一步根据lambda表达式获取MethodInfo和参数。要注意的是,对于属性的调用我们也需要支持,其实属性也是一种方法,所以可以一并处理。

    private static (MethodInfo method, IReadOnlyList<Expression> args) GetMethodAndArguments(Expression exp) => exp switch
    {
        LambdaExpression lambda => GetMethodAndArguments(lambda.Body),
        UnaryExpression unary => GetMethodAndArguments(unary.Operand),
        MethodCallExpression methodCall => (methodCall.Method!, methodCall.Arguments),
        MemberExpression { Member: PropertyInfo prop } => (prop.GetGetMethod(true) ?? throw new MissingMethodException($"No getter in propery {prop.Name}"), Array.Empty<Expression>()),
        _ => throw new InvalidOperationException("The expression refers to neither a method nor a readable property.")
    };

     

    第二步,利用Type.GetInterfaceMap获取到需要调用的接口实现方法。此处注意的要点是,instanceType.GetInterfaceMap(interfaceType).InterfaceMethods 会返回该接口的所有方法,所以不能仅根据方法名去匹配,因为可能有各种重载、泛型参数、还有new关键字声明的同名方法,所以可以按照方法名+声明类型+方法参数+方法泛型参数唯一确定一个方法(即下面代码块中IfMatch的实现)

    internal readonly record struct InterfaceMethodInfo(Type InstanceType, Type InterfaceType, MethodInfo Method);
    
    private static MethodInfo GetInterfaceMethod(InterfaceMethodInfo info)
    {
        var (instanceType, interfaceType, method) = info;
        var parameters = method.GetParameters();
        var genericArguments = method.GetGenericArguments();
        var interfaceMethods = instanceType
            .GetInterfaceMap(interfaceType)
            .InterfaceMethods
            .Where(m => IfMatch(method, genericArguments, parameters, m))
            .ToArray();
    
        var interfaceMethod = interfaceMethods.Length switch
        {
            0 => throw new MissingMethodException($"Can not find method {method.Name} in type {instanceType.Name}"),
            > 1 => throw new AmbiguousMatchException($"Found more than one method {method.Name} in type {instanceType.Name}"),
            1 when interfaceMethods[0].IsAbstract => throw new InvalidOperationException($"The method {interfaceMethods[0].Name} is abstract"),
            _ => interfaceMethods[0]
        };
    
        if (method.IsGenericMethod)
            interfaceMethod = interfaceMethod.MakeGenericMethod(method.GetGenericArguments());
    
        return interfaceMethod;
    }

     

    第三步,用获取到的接口方法,构建DynamicMethod。其中的重点是使用OpCodes.Call,它的含义是以非虚方式调用一个方法,哪怕该方法是虚方法,也不去查找它的重写,而是直接调用它自身。

    private static DynamicMethod GetDynamicMethod(Type interfaceType, MethodInfo method, IEnumerable<Type> argumentTypes)
    {
        var dynamicMethod = new DynamicMethod(
            name: "__IL_" + method.GetFullName(),
            returnType: method.ReturnType,
            parameterTypes: new[] { interfaceType, typeof(object[]) },
            owner: typeof(object),
            skipVisibility: true);
    
        var il = dynamicMethod.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
    
        var i = 0;
        foreach (var argumentType in argumentTypes)
        {
            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Ldc_I4, i);
            il.Emit(OpCodes.Ldelem, typeof(object));
            if (argumentType.IsValueType)
            {
                il.Emit(OpCodes.Unbox_Any, argumentType);
            }
            ++i;
        }
        il.Emit(OpCodes.Call, method);
        il.Emit(OpCodes.Ret);
        return dynamicMethod;
    }

     

    最后,将DynamicMethod转为强类型的委托就完成了。考虑到性能的优化,可以将最终的委托缓存起来,下次调用就不用再构建一次了。

    完整的代码点这里 

     

    方法2:利用函数指针

    这个方法和方法1大同小异,区别是,在方法1的第二步,即找到接口方法的MethodInfo之后,获取其函数指针,然后利用该指针构造委托。这个方法其实是我最初找到的方法,方法1是其改进。在此就不多做介绍了

     

    方法3:利用Fody在编译时对接口方法进行IL的call调用

    方法1虽然可行,但是肉眼可见的性能损失大,即使是用了缓存。于是乎我利用Fody编写了一个插件InterfaceBaseInvoke.Fody

    其核心思想就是在编译时找到目标接口方法,然后使用call命令调用它就行了。这样可以把性能损失降到最低。该插件的使用方法可以参考项目介绍

     

    性能测试

    方法 平均用时 内存分配
    父类的base调用 0.0000 ns -
    方法1(DynamicMethod) 691.3687 ns 776 B
    方法2(FunctionPointer) 1,391.9345 ns 1,168 B
    方法3(InterfaceBaseInvoke.Fody 0.0066 ns -

     

    总结

    本文探讨了几种实现接口的base调用的方法,其中性能以InterfaceBaseInvoke.Fody最佳,在C#官方支持以前推荐使用。欢迎大家使用,点心心,谢谢大家。

  • 相关阅读:
    零基础自学游戏开发和软件开发先学什么知识点或课程?
    网络安全(黑客)自学
    git commit后,如何撤销commit
    echarts中tooltip设为渐变色与模糊背景滤镜
    LeetCode 0143. 重排链表
    网友联手点燃亚运火炬,“数字火炬手”背后的助推器是什么?
    文章标题编号
    JavaWeb购物系统(十二)搜索联想词的添加
    03 _ 复杂度分析(上):如何分析、统计算法的执行效率和资源消耗?
    ubuntu 20.04+ORB_SLAM3 安装并行全记录(无坑版)(二)
  • 原文地址:https://www.cnblogs.com/huoshan12345/p/16362157.html