为什么Func<...>和Action没有合并?

15

我经常发现自己想要传递一个带有返回值但没有输入的Func来代替Action,例如:

Func<int> DoSomething = ...;

Task.Run(DoSomething);

我并不真的关心 DoSomething 的返回值。

但是这些类型不能合一,因此我最终会将调用进行封装。

Task.Run(() => { DoSomething(); });

有没有方法可以让这些类型统一,而不需要包装?另外,它们不统一的设计原因是否合理?


5
如果C#设计了某种Unit类型而不是"void",那么它们可以统一。然后,一个"Action"就可以简单地成为一个"Func<Unit>"。 - Dennis_E
2
如果你发现自己经常将 Func 作为 Action 传递,为什么不使用 F# 呢?与 C# 不同,F# 是从头开始设计的函数式语言。C# 简单地携带了很多来自其不太函数式的过去的包袱。正如 void 和函数接受多个参数一样:P 如果你只想简化你的代码,只需创建一个 AsAction 扩展方法,你就可以了。 - Luaan
4
Task.Run(DoSomething) 的问题在哪里?它会返回一个 Task<int>,但仍然是一个 Task。当你调用 Task.Run(Action)时,你得到的也是一个 Task - Sriram Sakthivel
2
@SriramSakthivel - 看起来不像是一种类型,也不像是一种类型的行为,所以根据鸭子类型(双关语),它不是一种类型:D - SWeko
2
@SriramSakthivel:System.Void仅用于反射。就语法而言,C#中的void绝对不是一种类型。语法中每个需要类型的地方都有一个特殊情况来处理void(除非void不允许,例如在类型参数中)。 - Joey
显示剩余4条评论
2个回答

6

你希望以下陈述为真:

如果我有一个 Func<T>,那么我应该能够在需要 Action 的地方使用它。

这将要求 Func<T> 要么是(A)可分配给 Action,要么是(B)隐式转换为 Action

如果我们假设(A),那就需要 T(可以是任何类型)可分配给 void

Eric Lippert 在 他的博客 中回答了这个问题:

对于从方法组到委托类型的协变返回类型转换,不应该将“void”视为所有可能类型的超类型吗?

他的答案是“不”,因为这与CLI规范不兼容。CLI规范要求返回值放在堆栈上,所以void函数最终不会生成“pop”指令,而那些有返回值的函数则会生成“pop”指令。如果有一种“操作”方式可以包含一个在编译时未知的void函数或返回某些内容的函数,则编译器将不知道是否生成“pop”指令。
他接着说:
如果CLI规范说“任何函数的返回值都在‘虚拟寄存器’中传递回来”,而不是将其推送到堆栈上,那么我们就可以使返回void的委托与返回任何内容的函数兼容。你总可以忽略寄存器中的值。但是这不是CLI规定的,所以我们不能这样做。
换句话说,如果存在一个“虚拟寄存器”来存储函数的返回值(这似乎不在CLI规范中),C#和其编译器的编写者可以做到你想要的,但由于他们不能偏离CLI规范,所以他们无法这样做。
如果我们假设(B),就会出现破坏性变化,正如Eric Lippert在this blog中解释的那样。将他博客中的示例适应到这里,如果存在从Func<T>Action的隐式转换,一些原本可以编译的程序将无法编译(破坏性变化)。该程序目前可以编译,但尝试取消注释隐式转换运算符,类似于您要求的内容,它将无法编译。
public class FutureAction
{
    public FutureAction(FutureAction action)
    {
    }

    //public static implicit operator FutureAction(Func<int> f)
    //{
    //    return new FutureAction(null);
    //}

    public static void OverloadedMethod(Func<FutureAction, FutureAction> a)
    {
    }

    public static void OverloadedMethod(Func<Func<int>, FutureAction> a)
    {
    }

    public static void UserCode()
    {
        OverloadedMethod(a => new FutureAction(a));
    }
}

(This isn't exactly what they'd be doing, obviously, since this only works for Func<int> and not Func<T>, but it illustrates the problem.)
摘要
我认为你面临的问题是CLI规范的产物,这可能是当时没有预见到的,我猜想他们不想引入破坏性变化来允许隐式转换,以使其正常工作。

我不同意T必须可分配给Void才能使其工作。你所说的是Func必须可以转换为Action。这并不正确。另一个可能的机制是隐式转换。 - Aron
@Aron。如果我在我的回答中将“可分配”一词更改为“隐式转换”,那么这样做是否正确?(我认为强制转换是指使用强制转换运算符,OP不想使用,所以我不想提到强制转换。) - Jesus is Lord
我的观点是,有多种机制可以将不同类型的对象分配给变量。当您将int值分配给double变量时,编译器知道您需要执行更多操作而不仅仅是赋值。会生成更多的IL代码。我认为在不改变CLI的情况下也可以实现相同的功能。 - Aron
好答案。@Aron,为了解决你的担忧,还有一种可能性是选项(B)会自动执行你现在正在手动执行的包装隐式转换。Visual Basic就是这样做的;C#不会。C#团队认为,如果两个引用类型之间的隐式转换不能保持引用标识,则会对大多数引用类型之间的隐式转换造成困惑。由于"手动"代码对开发人员来说并不是很繁琐,因此被认为是一个合理的选择。 - Eric Lippert
1
一个合理的问题是,为什么 VB 和 C# 设计团队会在这一点上存在差异 -- 当然,这两个团队有共同的成员!这归结于每种语言的设计哲学,很难在短评中概括。 - Eric Lippert

1

引用自CLI标准

II.4.6.1 委托签名兼容性

委托只能被验证地绑定到目标方法,其中:

  1. 目标方法的签名是可以分配给委托的签名;

...

如果满足以下所有条件,则类型为T的目标方法或委托可以分配给类型为D的委托:

  1. T的返回类型U和D的返回类型V,V可分配给U。

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