C#实现接口base调用示例详解

 更新时间:2022年06月10日 16:10:51   作者:返回主页月光双刀  
这篇文章主要为大家介绍了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&lt;IService&gt;(m =&gt; m.Proccess());
        Console.WriteLine("End Proccess");
    }
}

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

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

private static (MethodInfo method, IReadOnlyList&lt;Expression&gt; args) GetMethodAndArguments(Expression exp) =&gt; exp switch
{
    LambdaExpression lambda =&gt; GetMethodAndArguments(lambda.Body),
    UnaryExpression unary =&gt; GetMethodAndArguments(unary.Operand),
    MethodCallExpression methodCall =&gt; (methodCall.Method!, methodCall.Arguments),
    MemberExpression { Member: PropertyInfo prop } =&gt; (prop.GetGetMethod(true) ?? throw new MissingMethodException($"No getter in propery {prop.Name}"), Array.Empty&lt;Expression&gt;()),
    _ =&gt; 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 =&gt; IfMatch(method, genericArguments, parameters, m))
        .ToArray();
    var interfaceMethod = interfaceMethods.Length switch
    {
        0 =&gt; throw new MissingMethodException($"Can not find method {method.Name} in type {instanceType.Name}"),
        &gt; 1 =&gt; throw new AmbiguousMatchException($"Found more than one method {method.Name} in type {instanceType.Name}"),
        1 when interfaceMethods[0].IsAbstract =&gt; throw new InvalidOperationException($"The method {interfaceMethods[0].Name} is abstract"),
        _ =&gt; interfaceMethods[0]
    };
    if (method.IsGenericMethod)
        interfaceMethod = interfaceMethod.MakeGenericMethod(method.GetGenericArguments());
    return interfaceMethod;
}

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

private static DynamicMethod GetDynamicMethod(Type interfaceType, MethodInfo method, IEnumerable&lt;Type&gt; 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 ns776 B
方法2(FunctionPointer)1,391.9345 ns1,168 B
方法3(InterfaceBaseInvoke.Fody0.0066 ns-

总结

本文探讨了几种实现接口的base调用的方法,其中性能以InterfaceBaseInvoke.Fody最佳,在C#官方支持以前推荐使用,更多关于C#实现接口base调用的资料请关注脚本之家其它相关文章!

相关文章

  • C#远程发送和接收数据流生成图片的方法

    C#远程发送和接收数据流生成图片的方法

    这篇文章主要介绍了C#远程发送和接收数据流生成图片的方法,涉及C#通过数据流传输图片的相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-07-07
  • C#.NET采用HTML模板发送电子邮件完整实例

    C#.NET采用HTML模板发送电子邮件完整实例

    这篇文章主要介绍了C#.NET采用HTML模板发送电子邮件的方法,主要包括了HTML模板、替换函数与邮件函数三部分,是非常实用的功能,需要的朋友可以参考下
    2014-09-09
  • C#零基础开发中最重要的概念总结

    C#零基础开发中最重要的概念总结

    这篇文章主要为大家详细介绍了C#零基础开发中最重要的一些概念,文中的示例代码讲解详细,对我们学习C#有一定的帮助,需要的可以参考一下
    2023-02-02
  • C#文件断点续传实现方法

    C#文件断点续传实现方法

    这篇文章主要介绍了C#文件断点续传实现方法,涉及C#文件传输的技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-08-08
  • 详解C#实例化对象的三种方式及性能对比

    详解C#实例化对象的三种方式及性能对比

    这篇文章主要介绍了C#实例化对象的三种方式及性能对比,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-12-12
  • C#实现窗体中动态按钮的设计方法

    C#实现窗体中动态按钮的设计方法

    在窗体界面中,通常以按钮来代替菜单栏的功能,这种形式虽然给用户一种直观、界面风格各异的感觉,但通常按钮都是以静止的形式显示,所以本文给大家介绍了C#实现窗体中动态按钮的设计方法,感兴趣的朋友可以参考下
    2024-04-04
  • C#把EXCEL数据转换成DataTable

    C#把EXCEL数据转换成DataTable

    这篇文章介绍了C#把EXCEL数据转换成DataTable的方法,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-04-04
  • C#中datatable去重的方法

    C#中datatable去重的方法

    这篇文章主要介绍了C#中datatable去重的方法,通过两种不同的方法对比分析了datatable去重的技巧,非常具有实用价值,需要的朋友可以参考下
    2014-10-10
  • C#、vb.net及SQL判断指定年份是否为闰年的方法

    C#、vb.net及SQL判断指定年份是否为闰年的方法

    这篇文章主要介绍了C#、vb.net及SQL判断指定年份是否为闰年的方法,分别以三种方法实现了判断闰年的功能,在进行项目开发中具有一定的参考借鉴价值,需要的朋友可以参考下
    2014-11-11
  • C# .NET创建虚拟目录的方法详解

    C# .NET创建虚拟目录的方法详解

    虚拟目录(virtual directory),计算机术语,每个 Internet服务可以从多个目录中发布。本文将为大家介绍如何通过C# asp.net mvc 创建虚拟目录,感兴趣的可以了解一下
    2022-01-01

最新评论