方法的三种调用形式

在《可以调用Null的实例方法吗?》一文中,我谈到.NET方法的三种调用形式,现在我们就来着重聊聊这个话题。具体来说,这里所谓的三种方法调用形式对应着三种IL指令:Call、CallVirt和Calli。

一、三个方法调用指令
二、三种方法调用形式
三、虚方法的分发(virtual dispatch)
四、性能差异

一、三个方法调用指令

虽然C#的方法具有静态方法和实例方法之分,但是在IL层面,它们之间并没有什么不同,就是单纯的“函数”而已,而且这个函数的第一个参数的类型永远是方法所在的类型。所以在IL层面,方法总是“静态”的,调用实例方法的本质就是将目标实例作为第一个参数,对于静态方法,第一个参数永远是Null/Default(值类型)。我在《实例方法和静态方法有区别吗?》中曾经着重谈到过这个问题。

Call和CallVirt指令执行方法的流程只有两步:将所有参数压入栈中 + 执行方法。它们之间的不同之处在于:Call指令编译时就已经确定了执行的方法,而CallVirt则是在运行时根据作为第一个参数的实例类型决定最终执行的方法。Calli指令则有所不同,我们执行该指令时需要指定目标方法的指针,整个流程包括三步:将所有参数压入栈中 + 将目标方法指针压入栈中+执行方法

二、三种方法调用形式

接下来我们使用动态方法的形式演示上述三种方法调用指令的使用。具体来说,我们采用三种方式调用定义在Calculator中用来进行加法运算的Add方法,为此我们利用CreateInvoker方法根据指定的指令生成一个对应的Func<Calculator, int, int, int>委托。在CreateInvoker方法中,我们创建一个与Func<Calculator, int, int, int>委托匹配的动态方法。在IL Emit过程中,我们先将三个参数(Calculator对象和Add方法的参数a和b)压入栈中。如果指定的是Call和CallVirt指令,我们直接执行它们就可以了。如果指定的是Calli指令,我们得执行Ldftn指令将Add方法的指针压入栈中(方法指针通过指定的MethodInfo对象提供),然后再执行Calli指令。

var calculator = new Calculator();var invoker = CreateInvoker(OpCodes.Call);
Console.WriteLine($"1 + 2 = {invoker(calculator, 1, 2)} [Call]");invoker = CreateInvoker(OpCodes.Callvirt);
Console.WriteLine($"1 + 2 = {invoker(calculator, 1, 2)} [Callvirt]");invoker = CreateInvoker(OpCodes.Calli);
Console.WriteLine($"1 + 2 = {invoker(calculator, 1, 2)} [Calli]");static Func<Calculator, int, int, int> CreateInvoker(OpCode opcode)
{var method = typeof(Calculator).GetMethod("Add")!;var dynamicMethod = new DynamicMethod("Add", typeof(int), [typeof(Calculator), typeof(int), typeof(int)]);var il = dynamicMethod.GetILGenerator();il.Emit(OpCodes.Ldarg_0);il.Emit(OpCodes.Ldarg_1);il.Emit(OpCodes.Ldarg_2);if (opcode == OpCodes.Call){il.Emit(OpCodes.Call, method);}else if (opcode == OpCodes.Callvirt){il.Emit(OpCodes.Callvirt, method);}else if (opcode == OpCodes.Calli){il.Emit(OpCodes.Ldftn, method);il.EmitCalli(OpCodes.Calli, CallingConvention.ThisCall, typeof(int), [typeof(Calculator), typeof(int), typeof(int)]);}il.Emit(OpCodes.Ret);return (Func<Calculator, int, int, int>)dynamicMethod.CreateDelegate(typeof(Func<Calculator, int, int, int>));
}public class Calculator
{public virtual int Add(int a, int b) => a + b;
}

演示程序利用指定的三种方法指令创建了对应的Func<Calculator, int, int, int>,然后指定相同的参数(Calculator实例、整数1、2)执行它们,我们最终会在控制台上得到如下的输出结果。

image

三、虚方法的分发(virtual dispatch)

虽然Calculator的Add是个虚方法,由于Call指令执行的目标方法在编译时就确定,Calli则是我们以指针的形式指定了执行的方法,不论我们指定的目标对象具体是何类型,执行的永远是定义在Calculator类型的那个Add方法。面向对象“多态”的能力只能通过CallVirt指令来实现。

var calculator = new FakeCalculator();var invoker = CreateInvoker(OpCodes.Call);
Console.WriteLine($"1 + 2 = {invoker(calculator, 1, 2)} [Call]");invoker = CreateInvoker(OpCodes.Callvirt);
Console.WriteLine($"1 + 2 = {invoker(calculator, 1, 2)} [Callvirt]");invoker = CreateInvoker(OpCodes.Calli);
Console.WriteLine($"1 + 2 = {invoker(calculator, 1, 2)} [Calli]");public class FakeCalculator : Calculator
{public override int Add(int a, int b) => a - b;
}

以如上的程序为例,我们定义了Calculator的派生类FakeCalculator,在重写的Add方法中执行“减法运算”。我们将这个FakeCalculator对象作为参数调用三个委托,会得到如下所示的输出结果,可以看出CallVirt指令才能得到我们希望的结果。
image

四、性能差异

既然Call、CallVirt和Calli都是能帮助我们完成方法的执行,我们自然会进一步关系它们的性能差异了,为此我们来做一个简单的性能测试。

BenchmarkRunner.Run<Test>();public class Test
{private static readonly Func<Calculator, int, int, int> _call = CreateInvoker(OpCodes.Call);private static readonly Func<Calculator, int, int, int> _callvirt = CreateInvoker(OpCodes.Callvirt);private static readonly Func<Calculator, int, int, int> _calli = CreateInvoker(OpCodes.Calli);private static readonly Calculator _calculator = new FakeCalculator();[Benchmark]public int Call() => _call(_calculator, 1, 2);[Benchmark]public int Callvirt() => _callvirt(_calculator, 1, 2);[Benchmark]public int Calli() => _calli(_calculator, 1, 2);
}

如上所示的测试程序很简单,我们调用CreateInvoker方法将针对三种指令的Func<Calculator, int, int, int>委托和目标对象FakeCalculator创建出来,并在三个Benchmark方法中执行它们。从如下的测试结果可以看出,Call由于不需要进行”虚方法分发(Virtual Dispatch)”性能会比Callvirt执行好一些,但总体来说差别不大,但是Calli指令调用方法的性能会差很多
image

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/784116.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

关于隐藏Selenium绕过检测

. 浏览器指纹识别:网站通常通过浏览器指纹识别来检测访问者的身份。浏览器指纹是浏览器在访问网站时提供的一组信息,包括浏览器类型、版本、插件、用户代理字符串、屏幕分辨率、语言设置、操作系统等。 当你使用 Selenium 或其他自动化工具时,某些指纹信息可能会暴露自动化工…

依赖倒置原则

一、前言 依赖倒置原则也称依赖倒转原则(Dependence Inversion Principle) 看官方定义 高层模块不应该依赖底层模块,二者都应该依赖其抽象 抽象不应该依赖细节,细节应该依赖抽象 依赖倒置的中心思想是面向接口编程 如果你了解点设计模式,应该理解上面的话,但是如果不了解…

常见温升测试方法

常见温升测试方法 温升测试是电器产品安规测试项目之一,是为了检测电器产品及部件的温度变化情况,判断是否符合要求。在设备运行过程中会释放一定的热量,如果内部温度过高会影响产品的性能和稳定性,导致绝缘性能下降,因此温升测试是确保产品稳定运行的重要步骤。那么如何进…

当代码遇上诈骗,开发者该何去何从?

大家好,我是晓凡。 不知道大家最近有没有刷到这样一则消息:知名开源工具 Aria 的开发者删库跑路。 是的,你没听错,这不是段子,这是真事儿! 一言不合就删库? 这事儿得从一条令人震惊的提交记录说起。开发者留下的话,简直比冬天的北风还要刺骨:“因为自己的开源项目被诈…

5 个有趣的 Python 开源项目「GitHub 热点速览」

本期,我从上周的开源热搜项目中精心挑选了 5 个有趣、好玩的 Python 开源项目。 首先是 PyScript,它可以让你直接在浏览器中运行 Python 代码,不仅支持在 HTML 中嵌入,还能安装第三方库。然后是用 Python 写的“魔法虫洞” magic-wormhole,这是一个无需服务器、通过一条命…

耗时9个月,1.34万行代码,这套分布式微服务架构项目,完结啦!

作者:小傅哥 博客:https://bugstack.cn沉淀、分享、成长,让自己和他人都能有所收获!😄大家好,我是技术UP主小傅哥。 耗时9个月,1.34万行代码,55节课程,全程视频手把手。这套微服务、分布式、DDD架构,涵盖了;抽奖、活动、积分、兑换,运用了分库分表、binlog同步数据…

JetBrains PhpStorm 2024.2 (macOS, Linux, Windows) - 高效智能的 PHP IDE

JetBrains PhpStorm 2024.2 (macOS, Linux, Windows) - 高效智能的 PHP IDEJetBrains PhpStorm 2024.2 (macOS, Linux, Windows) - 高效智能的 PHP IDE JetBrains 跨平台开发者工具 请访问原文链接:https://sysin.org/blog/jetbrains-phpstorm/,查看最新版。原创作品,转载请…

Viper:强大的Go配置解析库

1 介绍 Viper是适用于Go应用程序的完整配置解决方案。它被设计用于在应用程序中工作,并且可以处理所有类型的配置需求和格式。目前Star 26.6k, 它支持以下特性:设置默认值 从JSON、TOML、YAML、HCL、envfile和Java properties格式的配置文件读取配置信息 实时监控和重新读取配…

JetBrains GoLand 2024.2 (macOS, Linux, Windows) - 为 Go 开发者打造的完整 IDE

JetBrains GoLand 2024.2 (macOS, Linux, Windows) - 为 Go 开发者打造的完整 IDEJetBrains GoLand 2024.2 (macOS, Linux, Windows) - 为 Go 开发者打造的完整 IDE JetBrains 跨平台开发者工具 请访问原文链接:https://sysin.org/blog/jetbrains-goland/,查看最新版。原创作…

流媒体服务器如何让WebRTC支持H.265,同时又能支持Web Chrome硬解码、软解码:DataChannel+MSE+WASM解码H.265

为了这一整套的解决方案,调研+研发整整花费了差不多半年多的时间,需达成的目标:流媒体服务器端不需要将H.265转码成H.264,就能让Chrome解码播放H.265;注意:现在很多市面上的软硬件通过转码H.265成H.264的方式来支持WebRTC,个人理解,这既费硬件又是技术的倒退!Web JS解…

Avalonia 后台代码简单播放动画示例

本文将演示如何在 Avalonia 的后台代码里面创建 Animation 执行播放本文演示的内容是将界面里面的一个 TextBlock 控件,通过修改控件的 RenderTransform 的 TranslateTransform 执行平移 为了方便演示,先在 MainView.axaml 里面添加一个 TextBlock 控件,如下面代码。大家可以…

读软件开发安全之道:概念、设计与实施03威胁

读软件开发安全之道:概念、设计与实施03威胁1. 威胁 1.1. 威胁常常比事件本身更加可怖1.1.1. 索尔阿林斯基1.2. 威胁无处不在1.2.1. 如果妥善管理,我们也可以安然与威胁共存1.2.2. 我们自己没有几百万年进化而来的本能来防御软件方面的威胁1.3. 把视角从软件构建者转向攻击者…