背景

在三年前发布的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#官方支持以前推荐使用。欢迎大家使用,点心心,谢谢大家。

【抬杠C#】如何实现接口的base调用的更多相关文章

  1. 使用base.调用父类里面的属性

    使用base.调用父类里面的属性public class parent { public string a; }public class child :parent { public string g ...

  2. loadrunner做webservice接口之简单调用

    今天听大神讲了webservice做接口,我按照他大概讲的意思自己模拟实战了下,可能还有很多不对,一般使用webservice做接口,会使用到soapui,但是用了loadrunner以后发现lr很快 ...

  3. 使用接口的方式调用远程服务 ------ 利用动态调用服务,实现.net下类似Dubbo的玩法。

    分布式微服务现在成为了很多公司架构首先项,据我了解,很多java公司架构都是 Maven+Dubbo+Zookeeper基础上扩展的. Dubbo是Alibaba开源的分布式服务框架,它最大的特点是按 ...

  4. Java WebService接口生成和调用 图文详解>【转】【待调整】

    webservice简介: Web Service技术, 能使得运行在不同机器上的不同应用无须借助附加的.专门的第三方软件或硬件, 就可相互交换数据或集成.依据Web Service规范实施的应用之间 ...

  5. java接口对接——别人调用我们接口获取数据

    java接口对接——别人调用我们接口获取数据,我们需要在我们系统中开发几个接口,给对方接口规范文档,包括访问我们的接口地址,以及入参名称和格式,还有我们的返回的状态的情况, 接口代码: package ...

  6. .net WebServer示例及调用(接口WSDL动态调用 JAVA)

    新建.asmx页面 using System; using System.Collections.Generic; using System.Linq; using System.Web; using ...

  7. php的swoole扩展中onclose和onconnect接口不被调用的问题

    在用swoole扩展写在线聊天例子的时候遇到一个问题,查了不少资料,现在记录于此. 通过看swoole_server的接口文档,回调注册接口on中倒是有明确的注释: * swoole_server-& ...

  8. 根据URL获取参数值得出json结果集,对外给一个接口让别人调用

    背景:测试接口的时候,经常都是后端get\post请求直接返回json结果集,很想知道实现方式,虽然心中也大概了解如何实现,但还不如自己来一遍踏实! 先来看一下结果吧: 以下对一个web的get接口进 ...

  9. 《React后台管理系统实战 :三》header组件:页面排版、天气请求接口及页面调用、时间格式化及使用定时器、退出函数

    一.布局及排版 1.布局src/pages/admin/header/index.jsx import React,{Component} from 'react' import './header. ...

随机推荐

  1. Mybatis多表查询出现null字段

    写在前面 今天使用mybatis实现多表查询,记录一下其中遇到的坑 mybatis多表查询简介 mybatis多表查询主要有两个方式,通俗易懂的来说就是一个是查询少量属性(association),一 ...

  2. MongoDB 数据库开发规范

    MongoDB 数据库开发规范 转载自-落雨_ https://developer.aliyun.com/article/255536 简介: mongoDB库的设计 mongodb数据库命名规范:d ...

  3. Apache Zookeeper 使用-安装

    Apache Zookeeper 使用-安装 官方网站 https://zookeeper.apache.org/ 官方文档 下载地址 Zookeeper 是什么? ZooKeeper is a ce ...

  4. DevC++ 报错[Error] Id returned 1 exit status

    DevC++ 报错[Error] Id returned 1 exit status 起因 学校机房的计算机总是二次编译总是报错 报错提示 [Error] Id returned 1 exit sta ...

  5. Java学习day25

    今天学习了UDP数据发送实现以及URL下载网络资源 UDP实现两个端口数据传输: package com.Cra2iTeT.chat; import java.io.BufferedReader; i ...

  6. EmlParse:一款超轻量级的批量解析EML格式电子邮件的工具

    工具特点 1.绿色纯天然,无任何依赖库,文件大小不到150K: 2.可批量解析EML格式的电子邮件: 3.可提取EML文件中的正文和附件到指定目录: 4.可生成HTML格式的邮件列表清单,方便用户进行 ...

  7. 在centos 7 中 conda 环境和Python2.7 中安装远程jupyter

    折腾了半天,为了能够方便学习TensorFlow,搞了远程的jupyter,方便在本地使用它,今天填了不少坑. 装完后截图: 下面是一些步骤: 检查 Python 环境 CentOS 7.2 中默认集 ...

  8. python模块,邮件推送交换机error级别以上log,django前端展示

    前情概要 原syslog服务器只收集不推送日志,可以实时展示,服务器在海外内网,办公网做的有分流,到日志服务器的流量送到香港,其余流量国内,疫情期间在家办公,每次连接需要拨海外l2tp,挂着梯子访问国 ...

  9. sa-token 配置 CORS

    return new SaServletFilter() ... .setBeforeAuth(r -> { // 前置函数,在认证函数每次执行前执行 // 设置一些安全响应头之类的玩意 SaH ...

  10. 循环中的scanf处理了换行符怎么破

    这种情况一般在循环中要求输入一个字符时容易出现问题. 问题在于缓冲区,缓冲区中存留了换行符,所以... 运行下面代码: int main(void){ char a=0; while(1){ scan ...