首页 > 其他分享 >可以调用Null的实例方法吗?

可以调用Null的实例方法吗?

时间:2024-08-19 09:49:55浏览次数:12  
标签:调用 void System instance 实例 IL Null 方法

前几天有个网友问我一个问题:调用实例方法的时候为什么目标对象不能为Null。看似一个简单的问题,还真不是一句话就能说清楚的。而且这个结论也不对,当我们调用定义在某个类型的实例方法时,目标对象其实可以为Null。

一、从ECMA-335 Spec说起
二、Call V.S Callvirt
三、直接调用(C#)
四、静态方法
五、值类型实例方法
六、?.操作符
七、扩展方法

一、从ECMA-335 Spec说起

A method that is associated with an instance of the type is either an instance method or a virtual method (see §I.8.4.4). When they are invoked, instance and virtual methods are passed the instance on which this invocation is to operate (known as this or a this pointer).
The fundamental difference between an instance method and a virtual method is in how the implementation is located. An instance method is invoked by specifying a class and the instance method within that class. Except in the case of instance methods of generic types, the object passed as this can be null (a special value indicating that no instance is being specified) or an instance of any type that inherits (see §I.8.9.8) from the class that defines the method. A virtual
method can also be called in this manner. This occurs, for example, when an implementation of a virtual method wishes to call the implementation supplied by its base class. The CTS allows this to be null inside the body of a virtual method.

A virtual or instance method can also be called by a different mechanism, a virtual call. Any type that inherits from a type that defines a virtual method can provide its own implementation of that method (this is known as overriding, see §I.8.10.4). It is the exact type of the object (determined at runtime) that is used to decide which of the implementations to invoke.

上面这段文字节选自Common Language Infrastructure (CLI),我来简单总结一下:

  • 与某个类型实例关联的方法,也就是被我们统称为实例方法,其实进一步划分为Instance Method和Virtual Method。我觉得将它们称为非虚实例方法(Non-Virtual Instance Method)和虚实例方法(Virtual Instance Method)更清楚;
  • 从IL指令来看,方法有Call和Callvirt两种调用方式。两种实例方法类型+两种调用方式,所以一共就有四种调用场景;
  • Call指令直接调用声明类型的方法,实在编译时决定的;Callvirt指令调用的是目标对象真实类型的方法,只能在运行时确定。从原理上讲,Call指令避免了目标方法的动态分发,所以性能更好;
  • 以Call不要求目标对象为Null,因为目标方法在运行时就已经确定了,但以Callvirt指令需要根据指定的对象确定目标方法所在的类型,所以要求目标对象不能为Null。

我个人在补充几点:

  • 在CLR眼中其实并没有静态方法和实例方法的区别,这两种方法都会自动添加一个前置的参数,其类型就是方法所在的类型。当我们调用静态方法时,第一个参数总是Null(对于值类型就是default),调用实例方法时则将目标对象作为第一个参数;
  • 除了Call和Callvirt指令,方法调用还有Calli指令,它可以更具提供的方法指针和参数列表来调用目标方法;

二、Call V.S. Callvirt

我们来回答开篇提出的问题:不论是不是虚方法,只要以Call指令调用,就不要求目标对象不为null;但我们不能使用Callvirt指令调用Null的实例方法,不论它们是否为虚方法。我们使用下面这个例子要验证这一结论。

using System.Reflection.Emit;

Invoke(CreateInvoker(OpCodes.Call, "Foo"));
Invoke(CreateInvoker(OpCodes.Call, "Bar"));
Invoke(CreateInvoker(OpCodes.Callvirt, "Foo"));
Invoke(CreateInvoker(OpCodes.Callvirt, "Bar"));

static void Invoke(Action<Foobar?> invoker)
{
    try
    {
        invoker(null);
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
}

static Action<Foobar?> CreateInvoker(OpCode opcode, string methodName)
{
    DynamicMethod foo = new DynamicMethod(
        name: "Invoke",
        returnType: typeof(void),
        parameterTypes: [typeof(Foobar)]);
    var il = foo.GetILGenerator();
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(opcode,typeof(Foobar).GetMethod(methodName)!);
    il.Emit(OpCodes.Ret);
    return (Action<Foobar?>)foo.CreateDelegate(typeof(Action<Foobar?>));
}

public class Foobar
{
    public void Foo() => Console.WriteLine(this is null);
    public virtual void Bar() => Console.WriteLine(this is null);
}

如上面的代码片段所示,Foobar类中定义了Foo和Bar两个实例方法,前者为常规方法,后者为虚方法。CreateInvoker方法根据指定的方法调用指令和方法名创建了一个动态方法(DynamicMethod ),进而创建出调用指定方法的Action<Foobar> 委托。Invoke方法会在Try/Catch中执行指定Action<Foobar>委托,以确定方法调用是否成功完成。演示程序先后四次调用Invoke方法,分别演示了以Call/Callvirt指令调用常规方法/虚方法,如下所示的输出结果证实了我们的结论。

image

三、直接调用(C#)

那么在C#中调用常规方法和虚方法又会如何呢?为此我定义了如下两个静态方法Foo和Bar,然后根据它们创建了对应的Action<Foobar>委托作为参数调用Invoke方法。

using System.Reflection.Emit;

Invoke(Foo);
Invoke(Bar);
static void Foo(Foobar? foobar) => foobar!.Foo();
static void Bar(Foobar? foobar) => foobar!.Bar();

static void Invoke(Action<Foobar?> invoker)
{
    try
    {
        invoker(null);
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
}

public class Foobar
{
    public void Foo() => Console.WriteLine(this is null);
    public virtual void Bar() => Console.WriteLine(this is null);
}

从如下的输出结果可以看出,不管调用的方法是否为虚方法,都要求目标对象不为Null。

image

根据我们上面的结论,既然方法调用作了“空引用验证”,使用的方法调用指令就不可能是Call。如下所是的是静态方法Foo和Bar的IL代码,可以看出它们调用Foobar对象的Foo和Bar方法采用的指令都是Callvirt。

.method assembly hidebysig static
	void '<<Main>$>g__Foo|0_0' (
		class Foobar foobar
	) cil managed
{
	.custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (
		01 00 02 00 00
	)
	.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
		01 00 00 00
	)
	// Method begins at RVA 0x20b2
	// Header size: 1
	// Code size: 8 (0x8)
	.maxstack 8

	// foobar!.Foo();
	IL_0000: ldarg.0
	IL_0001: callvirt instance void Foobar::Foo()
	// }
	IL_0006: nop
	IL_0007: ret
} // end of method Program::'<<Main>$>g__Foo|0_0'
.method assembly hidebysig static
	void '<<Main>$>g__Bar|0_1' (
		class Foobar foobar
	) cil managed
{
	.custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (
		01 00 02 00 00
	)
	.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
		01 00 00 00
	)
	// Method begins at RVA 0x20bb
	// Header size: 1
	// Code size: 8 (0x8)
	.maxstack 8

	// foobar!.Bar();
	IL_0000: ldarg.0
	IL_0001: callvirt instance void Foobar::Bar()
	// }
	IL_0006: nop
	IL_0007: ret
} // end of method Program::'<<Main>$>g__Bar|0_1'

在我的记忆中(也可能是我记错了),针对常规非虚方法的调用指令,原来的编译器会使用Call指令,不知道从哪个版本开始统一是Callvirt指令了。其实也好理解,如果方法不涉及目标对象,我们就应该将其定义成静态方法,针对实例方法执行空引用验证其实是有必要的。

四、静态方法

我们在上面说过,静态方法和实例方法并没有什么不同,但是调用静态方法时指定的第一个参数总是Null,所以针对它们的调用就不可能使用Callvirt指令,而只能使用Call指定。如下所示的是静态方法Invoke的IL代码,可以参数针对Console.WriteLine方法的调用使用的指令就是Call。

.method assembly hidebysig static
	void '<<Main>$>g__Invoke|0_2' (
		class [System.Runtime]System.Action`1<class Foobar> invoker
	) cil managed
{
	.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
		01 00 00 00
	)
	.param [1]
		.custom instance void [System.Runtime]System.Runtime.CompilerServices.NullableAttribute::.ctor(uint8[]) = (
			01 00 02 00 00 00 01 02 00 00
		)
	// Method begins at RVA 0x20c4
	// Header size: 12
	// Code size: 31 (0x1f)
	.maxstack 2
	.locals init (
		[0] class [System.Runtime]System.Exception ex
	)

	// {
	IL_0000: nop
	.try
	{
		// {
		IL_0001: nop
		// invoker(null);
		IL_0002: ldarg.0
		IL_0003: ldnull
		IL_0004: callvirt instance void class [System.Runtime]System.Action`1<class Foobar>::Invoke(!0)
		// (no C# code)
		IL_0009: nop
		// }
		IL_000a: nop
		IL_000b: leave.s IL_001e
	} // end .try
	catch [System.Runtime]System.Exception
	{
		// catch (Exception ex)
		IL_000d: stloc.0
		// {
		IL_000e: nop
		// Console.WriteLine(ex.Message);
		IL_000f: ldloc.0
		IL_0010: callvirt instance string [System.Runtime]System.Exception::get_Message()
		IL_0015: call void [System.Console]System.Console::WriteLine(string)
		// (no C# code)
		IL_001a: nop
		// }
		IL_001b: nop
		IL_001c: leave.s IL_001e
	} // end handler

	IL_001e: ret
} // end of method Program::'<<Main>$>g__Invoke|0_2'

五、值类型实例方法

对于值类型实例方法的调用,由于目标对象不可能是Null,而且值类型也没有虚方法一说,所以使用的指令也应该是Call。

static void Do(Foobar foobar) => foobar.Do();
public struct Foobar
{
    public void Do() { }
}

上面定义的静态方法Do具有如下的IL代码,可以看出它调用结构体Foobar的同名方法使用的指令就是Call。

.method assembly hidebysig static
	void '<<Main>$>g__Do|0_0' (
		valuetype Foobar foobar
	) cil managed
{
	.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
		01 00 00 00
	)
	// Method begins at RVA 0x2064
	// Header size: 1
	// Code size: 9 (0x9)
	.maxstack 8

	// foobar.Do();
	IL_0000: ldarga.s foobar
	IL_0002: call instance void Foobar::Do()
	// }
	IL_0007: nop
	IL_0008: ret
} // end of method Program::'<<Main>$>g__Do|0_0'

六、?.操作符

在进行方法调用时,如果不确定目标对象是否为Null,按照如下的形式使用?.操作符就很有必要。

static string ToString(object? instance) => instance?.ToString() ?? "N/A";

?.操作符仅仅是一个语法糖而已,编译器会将上述代码翻译成如下的形式:

static string ToString(object? instance) => ((instance != null) ? instance.ToString() : null) ?? "N/A";

七、扩展方法

扩展方法是个静态方法,所以针对它们的调用时不会进行空引用验证的。但是扩展方法又是以实例方法形式进行调用的,所以我推荐在定义扩展方法的时候最好对传入的第一个参数进行空引用验证。

public static class FoobarExtesnions
{
    public static void ExtendedMethod(this Foobar foobar)
    {
        ArgumentNullException.ThrowIfNull(foobar, nameof(foobar));
        ...
    }
}

标签:调用,void,System,instance,实例,IL,Null,方法
From: https://www.cnblogs.com/artech/p/18362421/call_callvirt

相关文章

  • 使用SSMS连接和查询 SQL Server 实例
    简介SQLServerManagementStudio是用于管理SQLServer基础架构的集成环境。ManagementStudio提供用于配置、监视和管理SQLServer实例的工具。此外,它还提供了用于部署、监视和升级数据层组件(如应用程序使用的数据库和数据仓库)的工具以生成查询和脚本。 官方使用教程:https......
  • 易优tag TAG调用标签-EyouCms手册
    【基础用法】名称:tag功能:TAG调用语法:{eyou:tagsort='now'getall='0'row='100'}{$field.tag}{/eyou:tag}参数:aid=''文档ID,在内容页可以不设置该属性typeid=''栏目ID,调取某个栏目下的全部TAGrow='100'返回广告列表总数getall=''获取类......
  • 易优arcview单条文档调用标签-EyouCms手册
    [基础用法]名称:arcview功能:获取单条文档数据语法:{eyou:arcviewaid='文档ID'}{$field.title}{/eyou:arcview}参数:aid=''指定文档ID,如果没有指定则获取当前文档内容页的文档IDid=''可以任意指定循环里的变量名替代field,假设id='field1',模板调用如:{$field.title}变成{$......
  • 易优type指定栏目调用标签-EyouCms手册
    [基础用法]名称:type功能:获取指定栏目信息语法:{eyou:typetypeid='栏目ID'empty='暂时没有数据'}{$field.typename}{/eyou:type}参数:typeid=''指定栏目ID,如果没有指定则获取当前列表页的栏目IDtype='self'表示当前栏目type='top'表示当前栏目最顶级的一级栏目addfie......
  • Java 使用 CompletableFuture 简化异步调用
    使用CompletableFuture可以大大简化处理多线程之间的异步调用关系,如串行依赖、并行、聚合等等。CompletableFuture是对Future接口的扩展和增强,进行了丰富的接口方法扩展,完美的弥补了Future的不足。本篇博客通过代码的方式,展示CompletableFuture的常用方法,体验其强大灵......
  • JavaScript函数调用之多括号调用(精讲)
    目录fn()()调用形式fn1().fn2()调用形式也叫链式调用链式调用的特点常见用途(fn())()调用形式也叫立即调用函数(IIFE)关键点:用途:示例:几种多括号调用形式:fn()()()fn1().fn2()(fn())()fn()()调用形式定义:是一个JavaScript中常见的模式,通常用于函数式编程。它的......
  • Vue3编写一个可以用js调用的组件
    项目开发中基本都会用到组件库,但是设计稿样式和功能不一定和组件库相同,尤其像是消息提示弹窗、确认弹窗,各个项目都有自己的一套风格。如何封装一个自己的弹窗组件,且不需要每个用到弹窗的组件都需要引入这个弹窗组件,然后传参等等这些繁琐的步骤。而只需要使用简单的js就可以直接调......
  • Codesys 可视化简单实例
    Codesys可视化功能比较强大,熟练使用之后可以做出漂亮的界面,本实例介绍Codesy是可视化的简单应用:程序定义如下:PROGRAMPLC_PRGVAR   s1:STRING:='GoodMorning';   s2:STRING:='Hello';   s3:string:='';   bStart:BOOL:=FALSE;END_VAR程序代码如下:IF......
  • asp.net core 调用wps实现word转pdf
    安装wpshttps://www.wps.cn/创建.netcore控制项目添加com引用,搜索wps准备word,名字叫001.docxword转pdf编写代码namespaceWPSStu01{internalclassProgram{staticvoidMain(string[]args){Console.WriteLine("......
  • DzzOffice 修改未授权用户能调用组织部门信息问题
    问题描述无需登录,直接访问/index.php?mod=system&op=orgtree,出现组织部门信息解决方法一该方法是直接将system应用(系统组件)设置为仅允许登录用户使用文件:\dzz\system\config\config.php添加'allow_view'=>1,参数<?phpreturnarray('allow_robot'=>false,'allo......