为什么委托逆变不适用于值类型?

发布于 2024-09-30 18:05:59 字数 333 浏览 6 评论 0原文

此代码段未在 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[]。这显然与拳击有关,但我想知道细节。

This snippet is not compiled in LINQPad.

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

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

The compiler's error message is:

No overload for 'UserQuery.IsNull(object)' matches delegate 'System.Func'

It works for a string array, but doesn't work for int[]. It's apparently related to boxing, but I want to know the details.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

友谊不毕业 2024-10-07 18:05:59

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

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 的哲学是“做我的意思,而不是我说的”。两者都是完全合理的哲学;有趣的是,两种具有几乎相同功能集的语言由于设计原理而在这些小细节上有所不同。

The answer given (that there is no variance involving value types) is correct. The reason covariance and contravariance do not work when one of the varying type arguments is a value type is as follows. Suppose it did work and show how things go horribly wrong:

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

OK, what happens? f2 is reference-identical to f1. Therefore whatever f1 does, f2 does. What does f1 do? It puts a 32 bit integer on the stack. What does the assignment do? It takes whatever is on the stack and stores it in variable "ob".

Where was the boxing instruction? There wasn't one! We just stored a 32 bit integer into storage that was expecting not an integer but rather a 64 bit pointer to a heap location containing a boxed integer. So you've just both misaligned the stack and corrupted the contents of the variable with an invalid reference. Soon the process will go down in flames.

So where should the boxing instruction go? The compiler has to generate a boxing instruction somewhere. It can't go after the call to f2, because the compiler believes that f2 returns an object that has already been boxed. It can't go in the call to f1 because f1 returns an int, not a boxed int. It can't go between the call to f2 and the call to f1 because they are the same delegate; there is no 'between'.

The only thing we could do here is make the second line actually mean:

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

and now we don't have reference identity between f1 and f2 anymore, so what is the point of variance? The whole point of having covariant reference conversions is to preserve reference identity.

No matter how you slice it, things go horribly wrong and there is no way to fix it. Therefore the best thing to do is to make the feature illegal in the first place; there is no variance allowed on generic delegate types where a value type would be the thing that is varying.

UPDATE: I should have noted here in my answer that in VB, you can convert an int-returning delegate to an object-returning delegate. VB simply produces a second delegate which wraps the call to the first delegate and boxes the result. VB chooses to abandon the restriction that a reference conversion preserves object identity.

This illustrates an interesting difference in the design philosophies of C# and VB. In C#, the design team is always thinking "how can the compiler find what is likely to be a bug in the user's program and bring it to their attention?" and the VB team is thinking "how can we figure out what the user likely meant to happen and just do it on their behalf?" In short, the C# philosophy is "if you see something, say something", and the VB philosophy is "do what I mean, not what I say". Both are perfectly reasonable philosophies; it is interesting seeing how two languages that have almost identical feature sets differ in these small details due to design principles.

差↓一点笑了 2024-10-07 18:05:59

因为 Int32 是值类型,而逆变对值类型不起作用。

你可以试试这个:

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

Because Int32 is value type and contra-variance doesn't work on value types.

You can try this one:

(new **object**[]{0,1,2,3}).Where(IsNull).Dump();
錯遇了你 2024-10-07 18:05:59

它不适用于 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; }

It doesn't work for an int because there are not objects.

Try:

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; }
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文