前几天有个网友问我一个问题:调用实例方法的时候为什么目标对象不能为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指令调用常规方法/虚方法,如下所示的输出结果证实了我们的结论。

三、直接调用(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。

根据我们上面的结论,既然方法调用作了“空引用验证”,使用的方法调用指令就不可能是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));
...
}
}

可以调用Null的实例方法吗?的更多相关文章

  1. 类型,对象,线程栈,托管堆在运行时的关系,以及clr如何调用静态方法,实例方法,和虚方法(第二次修改)

    1.线程栈 window的一个进程加载clr.该进程可能含有多个线程,线程创建的时候会分配1MB的栈空间. 如图: void Method() { string name="zhangsan ...

  2. 反射-优化及程序集等(用委托的方式调用需要反射调用的方法(或者属性、字段),而不去使用Invoke方法)

    反射-优化及程序集等(用委托的方式调用需要反射调用的方法(或者属性.字段),而不去使用Invoke方法)   创建Delegate (1).Delegate.CreateDelegate(Type, ...

  3. JNI学习笔记_Java调用C —— 非Android中使用的方法

    一.学习笔记 1.java源码中的JNI函数本机方法声明必须使用native修饰. 2.相对反编译 Java 的 class 字节码文件来说,反汇编.so动态库来分析程序的逻辑要复杂得多,为了应用的安 ...

  4. Android 通过 JNI 访问 Java 字段和方法调用

    在前面的两篇文章中,介绍了 Android 通过 JNI 进行基础类型.字符串和数组的相关操作,并描述了 Java 和 Native 在类型和签名之间的转换关系. 有了之前那些基础,就可以实现 Jav ...

  5. JDK1.6 Java.lang.Null.Pointer.Exception

    先来看一下JDK1.6的API: NullPointerException (Java Platform SE 6) public class NullPointerException extends ...

  6. JNI由浅入深_7_c调用Java方法一

    1.在Java中声明方法 <span style="font-size:14px;">/** * javah -encoding utf-8 -jni com.exam ...

  7. JQuery extend()与工具方法、实例方法

    使用jQuery的时候会发现,jQuery中有的函数是这样使用的: $.get(); $.post(); $.getJSON(); 有些函数是这样使用的: $('div').css(); $('ul' ...

  8. 04 JVM是如何执行方法调用的(上)

    重载和重写 重载:同一个类中定义名字相同的方法,但是参数类型或者参数个数必须不同. 重载的方法在编译过程中就可完成识别.具体到每一个方法的调用,Java 编译器会根据所传入参数的生命类型来选取重载方法 ...

  9. MSIL实用指南-Action的生成和调用

    MSIL实用指南-Action的生成和调用 System.Action用于封装一个没有参数没有返回值的方法.这里生成需要Ldftn指令. 下面讲解怎生成如下的程序. class ActionTest ...

  10. C#测试对比不同类型的方法调用的性能

    一. 测试方法调用形式 1. 实例方法调用 2. 静态方法调用 3. 实例方法反射调用 4. 委托方法的Invoke调用 5. 委托方法的DynamicInvoke调用 6.委托方法的BeginInv ...

随机推荐

  1. FPGA对EEPROM驱动控制(I2C协议)

    本文摘要:本文首先对I2C协议的通信模式和AT24C16-EEPROM芯片时序控制进行分析和理解,设计了一个i2c通信方案.人为按下写操作按键后,FPGA(Altera EP4CE10)对EEPROM ...

  2. Debian安装Redis服务

    Debian安装Redis服务 安装命令 apt-get update apt-get install redis-server 等待安装完成 配置密码 编辑Redis的配置文件/etc/redis/ ...

  3. 全国产!全志A40i+Logos FPGA核心板(4核ARM Cortex-A7)硬件说明

    硬件资源 SOM-TLA40iF核心板板载ARM.FPGA.ROM.RAM.晶振.电源.LED等硬件资源,并通过B2B连接方式引出IO.核心板所有器件(包括B2B连接器)均采用国产工业级方案,国产化率 ...

  4. 光伏储能电厂设备连接iec61850平台解决方案

    在当今日益发展的电力系统中,光伏储能技术以其独特的优势逐渐崭露头角,成为可再生能源领域的重要组成部分.而在光伏储能系统的运行与监控中,通信协议的选择与实现则显得至关重要.本文将重点介绍光伏储能系统中的 ...

  5. Git ignore 忽略文件不起作用

    前提:拉取了项目上的代码,或者自己本地已经提交过了的文件.然后在 .gitignore 文件中添加了过滤,但是不管用,还是可以追踪 解决方案: 1.先删除本地的文件(可以备份到其他文件夹外) 2.然后 ...

  6. windows下rust环境的安装(现在是2023年5月份)

    在自己家电脑上安装一下rust,还是遇到一些问题,这里记录一下,免得后面再踩坑. 官方网站 获取主要信息还得靠官网,比如安装软件:) 地址是 https://www.rust-lang.org/zh- ...

  7. mysql 二进制的读取与写入

    插入语句 用binary转换函数可将字符串转为二进制 insert into mytable (id, bin) values(1, binary('abcdef')) 查询语句 用cast进行类型转 ...

  8. git分支学习笔记2-解决合并的冲突

    来源:https://www.liuhaolin.com/git/115.html git中合并冲突是在不同的分支中同一个文件的内容不同导致的,如果进行合并就会冲突.文件可能是新增的文件,比如在两个分 ...

  9. webpack4.15.1 学习笔记(一) — 基本概念

    目录 入口(entry) 出口(output) 加载器 Loaders 插件 Plugins 模式 webpack.config.js 配置 终终终终于下定决心,对你下手了,系统的学习一下. webp ...

  10. 基于Java:流浪动物领养信息系统设计实现(源码+lw+部署文档+讲解等)

    \n文末获取源码联系 感兴趣的可以先收藏起来,大家在毕设选题,项目以及论文编写等相关问题都可以给我加好友咨询 系统介绍: 现代经济快节奏发展以及不断完善升级的信息化技术,让传统数据信息的管理升级为软件 ...