15

此代码段未在 LINQPad 中编译。

void Main()
{
    (new[]{0,1,2,3}).Where(IsNull).Dump();
}

static bool IsNull(object arg) { return arg == null; }

编译器的错误信息是:

'UserQuery.IsNull(object)' 没有重载匹配委托 'System.Func'

它适用于字符串数组,但不适用于int[]. 它显然与拳击有关,但我想知道细节。

4

3 回答 3

42

给出的答案(没有涉及值类型的差异)是正确的。当可变类型参数之一是值类型时,协变和逆变不起作用的原因如下。假设它确实有效并显示了事情是如何严重错误的:

Func<int> f1 = ()=>123;
Func<object> f2 = f1; // Suppose this were legal.
object ob = f2();

好的,会发生什么?f2 与 f1 引用相同。因此,无论 f1 做什么,f2 都会做。f1 是做什么的?它将一个 32 位整数放入堆栈。任务是做什么的?它获取堆栈上的任何内容并将其存储在变量“ob”中。

拳击指导在哪里?一个都没有!我们只是将一个 32 位整数存储到存储中,它期望的不是整数,而是指向包含装箱整数的堆位置的 64 位指针。因此,您既未对齐堆栈,又使用无效引用破坏了变量的内容。很快,这个过程将付诸东流。

那么拳击指导应该去哪里呢?编译器必须在某处生成装箱指令。它不能在对 f2 的调用之后进行,因为编译器认为 f2 返回一个已经被装箱的对象。它不能进入​​对 f1 的调用,因为 f1 返回一个 int,而不是装箱的 int。它不能在对 f2 的调用和对 f1 的调用之间进行,因为它们是同一个委托;没有“之间”

我们在这里唯一能做的就是让第二行实际上意味着:

Func<object> f2 = ()=>(object)f1();

现在我们在 f1 和 f2 之间没有参考身份了,那么方差的意义何在?具有协变引用转换的全部意义在于保留引用标识

不管你怎么切它,事情都会出错,而且没有办法修复它。因此,最好的办法是首先使该功能非法;泛型委托类型不允许变化,其中值类型将是变化的东西。

更新:我应该在回答中指出,在 VB 中,您可以将返回 int 的委托转换为返回对象的委托。VB 简单地生成第二个委托,它将对第一个委托的调用包装起来并将结果装箱。VB 选择放弃引用转换保留对象身份的限制。

这说明了 C# 和 VB 设计理念的一个有趣差异。在 C# 中,设计团队一直在思考“编译器如何才能发现用户程序中可能存在的错误并引起他们的注意?” VB 团队正在思考“我们如何才能弄清楚用户可能想要发生的事情并代表他们去做?” 简而言之,C# 的哲学是“如果你看到了什么,就说什么”,而 VB 的哲学是“做我的意思,而不是我说的”。两者都是完全合理的哲学;有趣的是,由于设计原则,具有几乎相同功能集的两种语言在这些小细节上有何不同。

于 2010-11-04T15:45:45.537 回答
3

因为Int32是值类型,反方差对值类型不起作用。

你可以试试这个:

(new **object**[]{0,1,2,3}).Where(IsNull).Dump();
于 2010-11-04T12:08:39.093 回答
-1

它不适用于 int,因为没有对象。

尝试:

void Fun()
{
    IEnumerable<object> objects = (new object[] { 0, 1, null, 3 }).Where(IsNull);

    foreach (object item in objects)
    {
        Console.WriteLine("item is null");
    }
}

bool IsNull(object arg) { return arg == null; }
于 2010-11-04T12:05:25.723 回答