C#是否对带有await的if语句进行短路求值?

76
我相信C#会在能够确定结果时停止评估if语句的条件。例如:

我相信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 && 的哪一部分文档让您认为它会 永远 跳过短路? - Alexei Levenkov
8
@IanKemp:我认为你需要重新阅读Alexei所说的内容... - musefan
1
然而,短路和布尔逻辑并不相同。(<anyThing> && false) 也只能评估为false,但是C、C#、C++已经决定短路仅基于第一个参数。 - Hans Olsson
2
短路计算与 if 无关。无论在何处使用,&&|| 都会执行短路计算,例如 some_var = <expression1> && <expression2> - Barmar
显示剩余2条评论
4个回答

91

是的,它将被短路。你的代码与以下代码等效:

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

请注意,它甚至不会调用second_check直到first_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”,这一点经常被忽视。 - Sebastian Schumann
谢谢,这非常有用。实际上我并不想并行评估它们,我只是好奇 await 的工作原理。 - Aidan
请注意,如果您想要实现对所有内容的评估,可以使用&而不是&&更容易地实现:if (await first_check() & await second_check()) { ... } 这是因为&运算符不会绕过任何内容,而&&则会在结果明确且不能通过后续操作数更改时停止(即如果第一个操作数已经为false,则没有检查第二个操作数的必要)。|||(逻辑OR vs 快捷OR)也是如此,但这里的快捷方式意味着当第一个操作数为true时停止评估。 - Matt

68

这很容易检查。

尝试这段代码:

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); }

它输出“Tested”,除此之外没有其他内容。


30
这个测试表明,C#允许这种短路现象的发生(假设所使用的编译器符合要求)。但它并未表明这种短路现象是必须的,也就是程序员不能依赖它。 - nanoman
我猜这里的一些背景是,直接放置在一起的两个“await”语句实际上是按顺序执行的。尽管它们用于异步编程,但“await”的执行顺序本身永远不会改变。(如果它们位于不同的函数或其他地方,则显然不适用。) 因此,在考虑调用“second_check()”之前,它总是会先得到“first_check()”的结果。因此,短路评估总是按相同的顺序执行,而永远不会首先评估“second_check()”。 - Panzercrisis
1
@Panzercrisis 另一种思考这个问题的方式是,await 的整个重点在于使异步函数看起来像是同步的。当它们用于布尔表达式时,没有理由不这样做。 - Barmar

11

是的,它可以。您可以使用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的无参方法,其结果将是表达式first_check()的值。

      2. 该值应为Task<T>ValueTask<T>,因为这是等待表达式。

      3. 等待等待表达式以获取它最终产生的值。

    2. 和表达式的第一个操作数是否产生false?是的。不必计算第二个操作数。

    3. 此时,我们知道await first_check() && await second_check()的值也必须为false

我包含的一些检查是静态完成的(即在编译时完成)。但是,它们存在是为了使事情更清晰 —— 没有必要谈论编译,因为我们只看表达式的评估方式。

整个问题的重点在于C#不会在意表达式是否被等待 ——它仍然是and表达式的第一个操作数,并且按此顺序评估。只有当它产生true时,第二个操作数才会被计算。否则,整个and表达式被认为是false,因为它不能是其他情况。

这基本上是大多数编译器(包括Roslyn(实际使用C#完全编写的C#编译器)和解释器)的工作方式,尽管我隐藏了一些不重要的实现细节,例如await表达式的等待方式,您可以通过查看生成的字节码(您可以使用像这样的网站)自己理解。只是为了澄清,await表达式的工作方式相当复杂,它不适合在这个问题的主题中讨论。它值得一个单独的答案来正确解释,但我认为它并不重要,因为它纯粹是一个实现细节,不会使等待的表达式与正常表达式有任何不同的行为。

1
我对刚刚收到的踩票非常好奇。亲爱的踩票者,您是否介意在评论中给出踩票的原因? - Davide Cannizzo

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