【问题标题】:How can I force a throw to be a statement and not an expression (in a lambda expression)?如何强制 throw 成为语句而不是表达式(在 lambda 表达式中)?
【发布时间】:2019-01-15 23:08:56
【问题描述】:

从 C# 7.0 开始 throw 关键字既可以用作表达式,也可以用作语句,这很好。 不过,请考虑这些重载

public static void M(Action doIt) { /*use doIt*/ }
public static void M(Func<int> doIt) { /*use doIt*/ }

这样调用时

M(() => throw new Exception());

甚至像这样(带有语句 lambda)

M(() => { throw new Exception(); });

编译器选择了 M(Func) 重载,表明在此将 throw 视为表达式。 如何优雅而明确地强制编译器选择 M(Action) 重载?

一种方法是这样

M(() => { throw new Exception(); return; });

但是 return 语句的原因似乎并不明显,并且冒着被下一个开发人员更改的风险,尤其是在 Resharper 警告无法访问的代码之后。

(当然我可以更改方法命名以避免重载,但这不是问题。:-)

【问题讨论】:

  • 澄清问题:自 C# 3 以来,()=&gt;{throw...} 的版本始终可分配给Func&lt;int&gt;;这对于 C# 7 来说并不新鲜。有趣的事实:delegate { throw new Exception(); } 可分配给任何没有“out”参数的委托类型。

标签: c# lambda expression action func


【解决方案1】:

这与 lambda 是语句 lambda 还是表达式 lambda 无关(正如您将 lambda 从表达式 lambda 更改为语句 lambda 并且行为没有改变最简洁地展示的那样)。

您可以通过多种方式使 lambda 匹配多个可能的重载。这是特定于较新版本的,但自 C# 1.0 以来已经应用了其他方法(并且自从引入匿名方法以来,需要存在匿名方法的特定处理和由此产生的重载决议消歧)。

确定调用哪个重载的规则在 C# 规范的第 7.5.3.3 节中有详细说明。具体来说,当参数是匿名方法时,它总是更喜欢委托(或表达式)有返回值的重载,而不是没有返回值的重载。无论是语句 lambda 还是表达式 lambda,这都是正确的;它适用于任何形式的匿名函数。

因此,您要么需要通过使匿名方法对Func&lt;int&gt; 无效来防止重载匹配,要么显式强制类型为Action,这样编译器就不会自行消除歧义。

【讨论】:

  • 我认为所有代码路径都会抛出异常,因此不需要返回方法,这是 Func&lt;int&gt; 重载首先被视为候选的原因吗?并且一旦考虑,它总是通过返回值来击败Action 重载。这是一次有趣的互动。
  • @JonathonChase:没错。为了与Func&lt;int&gt; 兼容,每个有返回的点都必须返回与 int 兼容的东西,并且该条件是空缺的,因为所有零返回中的零都满足要求。 “有趣”是一个词。 “实施起来超级有趣”不是另一个。假设有很多测试用例要写。
【解决方案2】:

您可以为Action 添加一个演员表,尽管它确实有点LISP'y 带有所有括号:

M((Action)(() => throw new Exception()));

不理想,但如果您想要最大程度的清晰度:

Action thrw = () => throw new Exception();
M(thrw);

【讨论】:

    【解决方案3】:

    一种可能的方法是使用命名参数:

    public static void M(Action action) { /* do stuff */ }
    
    public static void M(Func<int> func) { /* do stuff */ }
    
    public static void Main()
    {
        M(action: () => throw new Exception());
    }
    

    这可能应该记录在代码中,以免让下一个开发人员感到惊讶,并且如 cmets 中所述,编写适当的自动化测试来验证是否调用了正确的重载。

    【讨论】:

    • 我建议使用自动化测试来证明 Main 调用了正确的重载,因此重命名参数名称的人确实会通过调用 Func 重载来破坏实现。
    • @ErikPhilips - OP 确实注意到重命名方法以避免重载是避免问题的简单方法(并且在实际代码库中需要考虑),但我认为它仍然是关于重载解决的有效问题。
    • @ErikPhilips 我希望我已经编写了许多方法,它们在不同的重载中同时接受一个动作和一个函数。 .NET 框架也有不少例子。我想说这是一个完全可以接受的模式,你应该准备好作为 C# 程序员来处理。
    • @ErikPhilips:这里的根本问题是 CLR 类型系统对“void”和所有其他类型进行了如此强烈的区分。如果有一个只有一个值的“Unit”类型(null),那么我们根本不需要Action;它只是Func&lt;Unit&gt;。使void 成为一个非常特殊的类型的决定对类型系统有很多影响。我们还缺少“从不正常返回”类型,它可能是总是抛出的函数的返回类型。
    • @ErikPhilips,作为 @Eric 所述应用 unit 概念的示例,请参阅 F# 文档中的 here
    【解决方案4】:

    要添加到给出的所有合理答案,这里有一个迷人的不合理答案:

    ((Action<Action>)M)(() => throw new Exception());
    

    任何出现的维护程序员都应该bake the noodle,他们会不理会它。看看你能不能弄清楚它为什么起作用。

    【讨论】:

    • 我应该对不合理的答案投反对票还是对矩阵参考投赞成票?
    • 也可以满员((Func&lt;Action&lt;Action&lt;Action&gt;&gt;&gt;)(() =&gt; x =&gt; x(() =&gt; throw new Exception())))()(M)。很清楚,很合理。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-19
    • 1970-01-01
    • 2011-07-07
    • 1970-01-01
    • 2019-05-02
    相关资源
    最近更新 更多