惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
L
LINUX DO - 最新话题
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Forbes - Security
Forbes - Security
博客园 - 司徒正美
Hugging Face - Blog
Hugging Face - Blog
W
WeLiveSecurity
Jina AI
Jina AI
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
N
News and Events Feed by Topic
V
V2EX
Stack Overflow Blog
Stack Overflow Blog
Engineering at Meta
Engineering at Meta
PCI Perspectives
PCI Perspectives
Martin Fowler
Martin Fowler
T
The Exploit Database - CXSecurity.com
F
Full Disclosure
WordPress大学
WordPress大学
S
Security Affairs
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
S
SegmentFault 最新的问题
P
Privacy International News Feed
IT之家
IT之家
M
MIT News - Artificial intelligence
G
GRAHAM CLULEY
Hacker News: Ask HN
Hacker News: Ask HN
D
DataBreaches.Net
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google Online Security Blog
Google Online Security Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
C
Check Point Blog
美团技术团队
Security Latest
Security Latest
Cyberwarzone
Cyberwarzone
N
News and Events Feed by Topic
MyScale Blog
MyScale Blog
H
Help Net Security
宝玉的分享
宝玉的分享
The Hacker News
The Hacker News
The Last Watchdog
The Last Watchdog
The Cloudflare Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
爱范儿
爱范儿
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
I
Intezer
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
AI
AI
I
InfoQ
N
News | PayPal Newsroom
TaoSecurity Blog
TaoSecurity Blog

博客园 - Artech

[MAF预定义的AIContextProvider-05]CompactionProvider——采用多种策略压缩对话历史 [MAF预定义的AIContextProvider-04]Mem0Provider——长期记忆基于的云端解决方案 [MAF预定义的AIContextProvider-03]ChatHistoryMemoryProvider——赋予Agent从经验中学习的能力 [MAF预定义的AIContextProvider-02]AgentSkillsProvider——将Agent Skills引入MAF [MAF预定义的AIContextProvider-01]TextSearchProvider——RAG在MAF中的实现 [MAF预定义ChatClient中间件-09]MessageInjectingChatClient-赋予工具消息注入的能力 [MAF预定义ChatClient中间件-08]OpenTelemetryChatClient-实现链路跟踪和性能监控 [MAF预定义ChatClient中间件-07]PerServiceCallChatHistoryPersistingChatClient——基于ReAct循环的一步一存档 [MAF预定义ChatClient中间件-06]利用ImageGeneratingChatClient开发专业图片生成Agent [MAF预定义ChatClient中间件-05]动态修改ChatOptions和请求消息 [MAF预定义ChatClient中间件-04]ReducingChatClient——精减对话历史又不丢失基本语义 [MAF预定义ChatClient中间件-03]CachingChatClient——利用缓存省钱省时间 [MAF预定义ChatClient中间件-02]FunctionInvokingChatClient——实现ReAct循环和人机交互的大功臣 [MAF预定义ChatClient中间件-01]LoggingChatClient——在调用LLM前后输出日志 [MAF的Agent管道详解-07]利用AIAgent中间件构建Agent管道 [MAF的Agent管道详解-06]ChatClientAgent对IChatClient和输入输出增强管道的整合 [MAF的Agent管道详解-06]ChatClientAgent对IChatClient和输入输出增强管道的整合 - Artech [MAF的Agent管道详解-05]对话历史的持久化和输入输出的增强 [MAF的Agent管道详解-04]如何让LLM按照要求的结构输出数据? [MAF的Agent管道详解-03]连接LLM的IChatClient对象 [MAF的Agent管道详解-02]IChatClient管道如何完美连接大模型? [MAF的Agent管道详解-01]塑智能体边界,从AIAgent抽象类开始 [对比学习LangChain和MAF-04]针对消息的设计 [对比学习LangChain和MAF-03]完全不同的Agent设计哲学 [对比学习LangChain和MAF-02]基本编程模式的差异(下篇) [对比学习LangChain和MAF-01]基本编程模式的差异(上篇) 我所理解的Python元模型 除了按值和引用,方法参数的第三种传递方式 方法的三种调用形式 自定义Key类型的字典无法序列化的N种解决方案 为什么ASP.NET Core的路由处理器可以使用一个任意类型的Delegate 深入解析ASP.NET Core MVC的模块化设计[下篇] 深入解析ASP.NET Core MVC的模块化设计[上篇] ASP.NET Core MVC应用模型的构建[4]: Action的选择 ASP.NET Core MVC应用模型的构建[3]: Controller的收集 ASP.NET Core MVC应用模型的构建[2]: 定制应用模型
可以调用Null的实例方法吗?
Artech · 2024-08-19 · via 博客园 - Artech

前几天有个网友问我一个问题:调用实例方法的时候为什么目标对象不能为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));
        ...
    }
}