C#:为什么传递null会导致Object[]重载(但仅在某些情况下)

本文关键字:重载 情况下 为什么 null Object | 更新日期: 2023-09-27 18:27:23

取消对下面标记行的注释将导致stackoverflow,因为重载解决方案有利于第二种方法。但在第二种方法的循环中,代码路径会占用第一个重载。

这是怎么回事?

private static void Main(string[] args) {
    var items = new Object[] { null };
    Test("test", items);
    Console.ReadKey(true);
}
public static void Test(String name, Object val) {
    Console.WriteLine(1);
}
public static void Test(String name, Object[] val) {
    Console.WriteLine(2);
    // Test(name, null); // uncommenting this line will cause a stackoverflow
    foreach (var v in val) {
        Test(name, v);
    }    
}

C#:为什么传递null会导致Object[]重载(但仅在某些情况下)

第二个调用按预期工作,因为第二个方法中的valObject[]类型,所以在foreach中,var v很容易被推断为Object类型。这里没有歧义。

第二个调用是不明确的:ObjectObject[]类型的引用都可以是null,所以编译器必须猜测你指的是哪一个(下面会详细介绍)。null没有自己的任何类型;如果是这样的话,你需要一个明确的演员阵容来做任何事情,这将是令人不快的。

重载解析发生在编译时,而不是运行时。循环中的过载分辨率有时不是基于v是否恰好是null;这要到运行时才能知道,也就是在编译器解决重载很久之后。它基于声明的v类型。v的声明类型是推断的,而不是显式声明的,但关键是它在编译时是已知的,当重载被解析时

在另一个显式传递null的调用中,编译器必须使用算法(这里是普通人希望理解的语言的答案)来推断您想要的重载,在这种情况下,该算法会得出错误的答案。

在这两者中,它选择Object[],因为Object[]可以强制转换为Object,但事实并非如此——Object[]"更具体",或更专业。它离类型层次结构的根更远(或者在通俗英语中,在这种情况下,其中一个类型是Object,另一个不是)。

为什么专业化是标准?假设给定两个相同名称的方法,具有更通用参数类型的方法将是一般情况(您可以将anything强制转换为Object),对于某些特定的情况,带有更靠近类型层次结构叶子的类型的重载将取代一般的case方法:"除非它是Object的数组,否则对所有事情都使用这个方法;我需要对对象数组做一些不同的事情"。

这并不是唯一可以想象的标准,但我想不出还有其他一半那么好。

在这种情况下,这是违反直觉的,因为你认为null是一种普遍的东西:它甚至不是具体的Object。这是随便

以下内容会起作用,因为在这里编译器不必猜测null:的意思

public static void Test(String name, Object[] val) {
    Console.WriteLine(2);
    Object dummy = null;
    Test(name, dummy);
    foreach (var v in val) {
        Test(name, v);
    }
}

简言之:显式nulls会把重载解析搞得一团糟,以至于我有时会想,让编译器来计算它们是否不是语言设计者的错误(注意:"我有时会怀疑它是否……"不是教条式的确定性表达;设计语言的人比我更聪明)。

编译器尽可能地聪明,这"不是很聪明"。它可能有零星的彻头彻尾的恶意,但这起案件只是善意出了问题。

重载时,如果遇到歧义,编译器将始终尝试执行最具体的方法。

在这种情况下,object[]object更具体。

null可以是任何类型,因此它匹配两个方法签名。由于编译器必须做出决定,它将选择Test(string name, Object[] val),从而导致StackOverflowException

然而,在foreach循环中,v被推断为object类型。请注意,现在您有了一个类型化变量。

作为objectv可以是objectobject[](或几乎任何类型),但编译器不知道这一点,至少在运行时才会知道。

重载是在编译时解决的,因此编译器唯一的线索是vobject,因此它将选择调用Test(string name, Object value);

如果你有以下行:

var val = new object[] { };
Test(name, val);

那么就会调用Test(string name, Object[] val),因为编译器在编译时知道valobject[]

在对Test(name, v);的调用中,您的变量v作为与其关联的Object的类型,即使它的值是null,我们仍然知道我们有"什么类型的null"。

在对Test(name, null);的调用中,null没有与其关联的类型,因此编译器会找到最接近的匹配项并使用该重载。最接近的匹配是object[]过载。