扩展方法和成员方法:为什么编译器在内部实现时会有不同的实现方式?

5

考虑以下代码:

 A a = null;
 a.f(); //Will it throw NullReferenceException?

以上代码会抛出NullReferenceException吗?
答案是:这取决于f()是什么。
- 如果它是成员方法,则是的,它会抛出异常。 - 如果它是扩展方法,则不会抛出任何异常
这种差异引发了一个问题:每种类型的方法如何被C#编译器实现和查看?此外,为什么即使成员方法不访问任何成员数据,它也必须抛出异常?似乎C#编译器提前做出了一个假设,即成员方法将访问成员数据,因此如果对象为空,它就会抛出异常,因为使用空对象无法访问成员数据。但是,在扩展方法的情况下,它会推迟此决定,直到它尝试使用空引用访问成员数据时,然后才会抛出异常
我的理解有多正确?如果是这样,为什么会有这种差异?
是的,我知道如果f()是扩展方法,则a.f()等同于编写AExt.f(a),因此后者在使用a访问成员之前不应抛出异常。但我的重点主要是编译器实现(甚至可以以相同的方式实现成员方法)。

1
扩展方法只是同义词,就像你所说的那样。 - bevacqua
正如Nico所说,这只是普通静态方法调用的语法糖。其中一个方便之处是:它使得在字符串上定义.IsNullOrEmpty方法成为可能,从而允许s.IsNullOrEmpty() - Kirk Woll
@Kirk:s.IsNullOrEmpty() 是正确的吗?难道不应该是 string.IsNullOrEmpty(s) 吗?我认为你写的代码甚至无法编译! - Nawaz
@Nawaz,如果你定义了自己的扩展方法 IsNullOrEmpty(this string s),那么它就会起作用。 - svick
@svick:这就是我的问题。为什么它应该起作用,而当它是成员方法时为什么不起作用? - Nawaz
@Nawaz,虽然它看起来像是一个实例方法,但它被实现为静态方法。(只需查看生成的IL代码-好像扩展方法的概念根本不存在)“点”符号用于实例成员需要实际实例才能有任何意义。调用实例方法而没有有效的“this”引用是无意义的。对于具有空第一个参数的静态方法则完全不同。扩展方法语法重载了“点”符号的含义,以允许将临时静态方法与类型关联。 - Kirk Woll
2个回答

2
是的,这就是代码的行为方式(如果扩展方法没有检查null并自己抛出异常)。如果该方法不直接或间接访问类的任何实例字段,则在null上调用非虚拟实例方法可能有效。
但是,语言设计者认为这会令人困惑,所以他们通过使用callvirt IL指令确保对象不是null
对于扩展方法,这并不那么令人困惑(没有人期望thisnull,但应该检查声明为this IEnumerable<TSource> source的参数)。您可以像普通静态方法一样调用扩展方法,因此它应该始终检查null。此外,两种调用函数的方式应该完全相同。

我真的希望设计者们定义了一个属性,以便说明“这个方法可以使用空的this进行调用,并且会做出合理的操作”;这样的设计将允许封装值的不可变类类型表现得像值类型一样[例如,可以说myString.IsNullOrEmpty而不是String.IsNullOrEmpty(myString)]。 - supercat
@supercat 为什么?你可以很容易地将其编写为扩展方法。 - svick
扩展方法具有奇怪的作用域规则,特别是从反射的角度来看,它们实际上存在于与实例方法分离的单独宇宙中。如果有一种方法可以在类内声明扩展方法,那将会有所帮助,但据我所知,这是不允许的:扩展方法必须放在非嵌套非泛型静态类中,因此任何被扩展方法使用的类型或类型成员都必须在该级别可见,无论该方法是否公开暴露类型或成员的任何细节(甚至其存在)。 - supercat

1

实例方法在对象为空时必须抛出NullReferenceException,因为它们本质上保证了当执行该方法时对象是可用的。将实例方法视为对象实例执行的操作,当对象本身不存在时,该操作甚至无法被调用。扩展方法只是语法糖,编译器会立即将它们转换成你上面提到的AExt.f(a)。这只是一种方便,作为一种眼花缭乱的方式来帮助更好地阅读代码。

对于那些无论实例是否可用都想调用方法的情况,语言提供了静态方法作为特性。正如您所看到的,扩展方法就是静态方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接