【问题标题】:Why don't Func<...> and Action unify?为什么 Func<...> 和 Action 不统一?
【发布时间】:2016-01-14 00:39:48
【问题描述】:

例如,我发现自己一直想传递带有返回且没有输入的 Func 来代替 Action

Func<int> DoSomething = ...;

Task.Run(DoSomething);

哪里,我并不关心DoSomething的返回值。

然而,这些类型并没有统一,我最终将调用包装起来

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

有没有办法让这些类型统一而不用包装?另外,它们不统一是否有很好的设计原因?

【问题讨论】:

  • 如果 C# 被设计成某种 Unit 类型而不是 void,他们可以统一。那么Action 就是Func&lt;Unit&gt;
  • 如果您发现自己经常Func 传递为Action,为什么不使用F#?与 C# 不同,它从头开始设计为一种函数式语言。 C# 只是背负了过去功能不强的许多包袱。正如void 所证明的那样,以及带有多个参数的函数,例如:P 如果您只是想简化代码,只需为自己创建一些AsAction 扩展方法,就可以了。
  • Task.Run(DoSomething); 有什么问题?它会返回Task&lt;int&gt;,它仍然是Task。当您致电Task.Run(Action) 时,您会得到什么?
  • @SriramSakthivel - 它看起来不像一个类型,它也不像一个类型,所以根据鸭子打字(双关语),它不是一个类型:D
  • @SriramSakthivel:System.Void 仅用于反射。就语法而言,C# 中的void 绝对不是一种类型。语法中每个需要类型的地方都有 void 的特殊情况(除非 void 是不允许的,例如在类型参数中)。

标签: c# types anonymous-function unification


【解决方案1】:

CLI Standard拉取:

II.4.6.1 委托签名兼容性

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

  1. 目标方法的签名是delegate-assignable-to委托的签名;

...

T 类型的目标方法或委托是delegate-assignable-to当且仅当以下所有条件都适用时:

  1. T的返回类型U和D的返回类型V,V是assignable-toU。

【讨论】:

    【解决方案2】:

    您希望以下陈述为真:

    如果我有Func&lt;T&gt;,我应该可以在需要Action 的地方使用它。

    这要求Func&lt;T&gt;(A) 可分配给Action(B) 可隐式转换为Action

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

    Eric Lippert 在his blog 中回答了这个问题:

    出于从方法组到委托类型的协变返回类型转换的目的,“void”是否应该被视为所有可能类型的超类型?

    他的回答是“不”,因为这最终与 CLI 规范不兼容。 CLI 规范要求返回值进入堆栈,因此void 函数最终不会生成“pop”指令,而那些确实返回某些内容的函数会生成“pop”指令。如果有某种方法可以让“动作”包含void 函数或返回某些内容的函数,这在编译时是未知的,编译器将不知道是否生成“pop” "指令。

    他接着说:

    如果 CLI 规范规定“任何函数的返回值都在‘虚拟寄存器’中传回”,而不是将其压入堆栈,那么我们可以使返回 void 的委托与返回任何内容的函数兼容。您总是可以忽略寄存器中的值。但这不是 CLI 指定的,所以我们不能这样做。

    换句话说,如果存在存储函数返回值的“虚拟寄存器”(可能在 CLI 规范中不存在),C# 的编写者及其编译器可能已经完成了您想要的操作,但是他们不能,因为他们不能偏离 CLI 规范。

    如果我们假设 (B),就会发生重大变化,正如 Eric Lippert 在 this blog 中解释的那样。将他博客中的示例改编为这个,如果从Func&lt;T&gt;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));
        }
    }
    

    (显然,这不是他们要做的,因为这只适用于Func&lt;int&gt; 而不是Func&lt;T&gt;,但它说明了问题。)

    总结

    我认为您面临的问题是 CLI 规范的产物,当时可能没有预见到,我猜他们不想引入重大更改以允许隐式转换为工作。

    【讨论】:

    • 我不同意 T 必须可分配给 Void 才能工作。您所说的是 Func 必须可以转换为 Action。这不是真的。另一种可能的机制是隐式转换。
    • @Aron。如果我在答案中将“可分配”的措辞更改为“隐式可转换”,那是否正确? (I think casting refers to the usage of the casting operator,我相信 OP 不想使用它,所以我不想提及铸造。)
    • 我的观点是,有多种机制可以将对象分配给不同类型的变量。当您将 int 值分配给 double 变量时,编译器知道您需要做的不仅仅是分配。它产生了更多的IL。我是说在技术上可以在不更改 CLI 的情况下完成相同的操作。
    • 好答案。 @Aron,为了解决您的问题,选项(B)还有另一种可能性,那就是让隐式转换自动执行您现在“手动”执行的包装。 Visual Basic 就是这样做的; C# 没有。 C# 团队认为,如果两个引用类型之间的隐式转换不保留引用标识,就像大多数引用类型之间的隐式转换一样,那将是令人困惑的。由于“手动”代码对开发人员来说不是很大的负担,因此被认为是一个合理的替代方案。
    • 人们可能会合理地问为什么 VB 和 C# 设计团队——它们当然有共同点! ——在这一点上有所不同。它归结为每种语言的设计理念,很难用简短的评论来概括。
    猜你喜欢
    • 1970-01-01
    • 2023-03-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多