C#是否对if语句进行短路评估?


74

我相信C#能够判断结果后就立即停止评估if语句条件。因此,例如:

if ( (1 < 0) && check_something_else() )
    // this will not be called

由于条件的(1 < 0)值为false&&因此无法满足条件,check_something_else()因此不会调用该条件。

C#如何使用异步函数评估if语句?是否等待两者返回?因此,例如:

if( await first_check() && await second_check() )
    // ???

这会短路吗?


13
既不if也不await影响短路。async除了允许await 使用之外,不影响语言的行为,以等待已经执行的异步操作而不会阻塞。
Panagiotis Kanavos

14
其中一部分Operator &&文件让你认为它可以永远跳过短路?
阿列克谢·莱文科夫

8
@IanKemp:我认为您需要重新阅读Alexei所说的...
musefan

1
但是,短路和布尔逻辑并不相同。(<anyThing> && false)也只能计算为false,但是C,C#,C ++已确定短路仅基于第一个参数。
汉斯·奥尔森,

2
短路与无关if&&||在任何地方使用都执行短路,例如some_var = <expression1> && <expression2>
Barmar

Answers:


67

这是超级简单的检查。

试试这个代码:

async Task Main()
{
    if (await first_check() && await second_check())
    {
        Console.WriteLine("Here?");
    }
    Console.WriteLine("Tested");
}

Task<bool> first_check() => Task.FromResult(false);
Task<bool> second_check() { Console.WriteLine("second_check"); return Task.FromResult(true); }

它输出“已测试”,没有其他输出。


29
该测试表明C#允许这种短路(假定使用的编译器兼容)。它没有显示是否需要短路,即程序员可以依靠短路。
nanoman

我想这里的一些上下文是,两个await语句直接一个接一个地放置,基本上按顺序执行。即使将它们用于异步编程,它们await本身执行的顺序也不会改变。(如果它们在不同的函数中或某些地方,则显然不成立。)因此,first_check()在考虑调用之前,总是会得到返回的结果second_check()。因此,短路评估始终以相同的顺序执行,而无需second_check()先评估。
Panzercrisis

1
@Panzercrisis另一种思考方式是,它的全部意义await在于它使异步函数看起来是同步的。没有理由在布尔表达式中使用它们时应该有所不同。
Barmar

91

是的,它会短路。您的代码等效于:

bool first = await first_check();
if (first)
{
    bool second = await second_check();
    if (second)
    {
        ...
    }
}

请注意,直到by返回的等待操作完成后,它才调用 。因此请注意,这不会并行执行两个检查。如果您想这样做,可以使用:second_checkfirst_check

var t1 = first_check();
var t2 = second_check();

if (await t1 && await t2)
{
}

在那时候:

  • 这两个检查将并行执行(假设它们确实是异步的)
  • 它会等待第一个检查完成,然后只能等待第二次检查来完成,如果第一个返回true
  • 如果第一个检查返回false,但是第二个检查失败并带有异常,则该异常将被有效吞噬
  • 如果第二个检查真的很快就返回false,但是第一个检查需要很长时间,则整个操作将花费很长时间,因为它会等待第一个检查先完成

如果要并行执行检查,一旦它们中的任何一个返回false,就立即完成检查,则可能要为此编写一些通用代码,收集要开始的任务,然后Task.WhenAny重复使用。(您还应该考虑由于另一个任务返回false而实际上与最终结果无关的任务引发的任何异常所要发生的情况。)


14
明确声明的坦克在这里,如果第一个调用返回,则有效地吞没了异常false。如果有人使用,通常会错过这一点Task.WhenAny
塞巴斯蒂安·舒曼

谢谢,这很有用。我实际上并不想同时评估它们,我只是对await工作原理感到好奇。
艾丹

请注意,如果要评估所有内容,则可以使用&而不是来更轻松地实现&&if (await first_check() & await second_check()) { ... }这是因为&运算符将不会绕过任何内容,而&&如果结果是明确的且不会被后续操作数更改(例如,如果第一个操作数为,则停止操作)已经false,那么检查第二个操作数就没有意义了。与|和的情况相同||(逻辑OR与快捷方式OR),但此处的快捷方式表示当第一个操作数为时,求值停止true
马特

12

是的,它确实。您可以使用sharplab.io自己检查以下内容:

public async Task M() {
    if(await Task.FromResult(true) && await Task.FromResult(false))
        Console.WriteLine();
}

由编译器有效地转换为以下形式:

TaskAwaiter<bool> awaiter;

... compiler-generated state machine for first task...

bool result = awaiter.GetResult();

// second operation started and awaited only if first one returned true    
if (result)
{
     awaiter = Task.FromResult(false).GetAwaiter();
...

或作为一个简单程序:

Task<bool> first_check() => Task.FromResult(false);
Task<bool> second_check() => throw new Exception("Will Not Happen");

if (await first_check() && await second_check()) {}

第二个关于sharplab.io的例子。


3

由于我一直在自己编写编译器,因此我有资格提供更多的逻辑意见,而不仅仅是基于某些测试。

如今,大多数编译器将源代码转换为AST(抽象语法树),该算法用于以独立于语言的方式表示源代码。
AST通常由语法节点组成。产生值的语法节点称为表达式,而不产生任何值的语法节点称为语句。

给定问题中的代码,

if (await first_check() && await second_check())

让我们考虑测试条件表达式,即

await first_check() && await second_check()

为此类代码生成的AST将类似于:

AndExpression:
    firstOperand = (
        AwaitExpression:
            operand = (
                MethodInvocationExpression:
                    name = "first_check"
                    parameterTypes = []
                    arguments = []
            )
    )
    secondOperand = (
        AwaitExpression:
            operand = (
                MethodInvocationExpression:
                    name = "second_check"
                    parameterTypes = []
                    arguments = []
            )
    )

AST本身以及我用来表示它的语法是完全动态地发明的,因此,我希望它很清楚。看起来StackOverflow标记引擎喜欢它,因为它看起来不错!:)

在这一点上,要弄清楚的是将被解释的方式。好吧,我可以告诉大多数解释器只是按层次评估表达式。因此,可以通过以下方式完成:

  1. 评估表达 await first_check() && await second_check()

    1. 评估表达 await first_check()

      1. 评估表达 first_check()

        1. 解决符号 first_check

          1. 是参考吗?否(否则请检查它是否引用了委托。)
          2. 它是方法名称吗?是的(我不提供诸如解决嵌套范围,检查其是否为静态等之类的东西,因为它是离题的,问题中提供的信息不足,无法深入了解这些细节。)
        2. 评估论点。没有人 因此,将调用具有名称的无参数方法first_check

        3. 调用名为的无参数方法first_check,其结果将是expression的值first_check()

      2. 该值应为aTask<T>ValueTask<T>,因为这是一个await表达式。

      3. 正在等待await表达式以获取最终将产生的值。

    2. 和表达式的第一个操作数产生false吗?是。无需评估第二个操作数。

    3. 至此,我们知道的价值await first_check() && await second_check()必定也将false如此。

我包括的某些检查是静态完成的(即,在编译时)。但是,它们的存在是为了使事情更清楚—无需谈论编译,因为我们只是在考虑表达式的求值方式。

整件事的本质是C#不在乎表达式是否在等待-它仍然是and表达式的第一个操作数,因此它将首先被求值。然后,仅当它将产生true第二个操作数时,才会对其进行评估。否则,将整个和表达式假定为false,否则就不能这样。

这主要是绝大多数编译器(包括Roslyn(实际的C#编译器,完全使用C#编写))和解释器的工作方式,尽管我已经隐藏了一些无关紧要的实现细节,例如等待表达式的方式真的等了,你可以通过查看生成的字节码了解自己(你可以使用一个网站,像这样我无论如何不附属于这个网站-我只是建议,因为它使用了罗斯林,我认为这是一个不错的要牢记的工具。)

需要澄清的是,等待表达式工作的方式相当复杂,因此不适合该问题的主题。它应该有一个完整的,分开的答案来进行正确的解释,但是我不认为它很重要,因为它纯粹是实现细节,不会使等待的表达式的行为与正常表达式有所不同。


我对刚刚收到的反对票感到很好奇。亲爱的唐纳德·沃特,您介意对我的回答发表评论,以说明为何选择唐纳德·沃特?
Davide Cannizzo
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.