C# 是不是使用 await 对 if 语句执行短路评估?

Posted

技术标签:

【中文标题】C# 是不是使用 await 对 if 语句执行短路评估?【英文标题】:Does C# perform short circuit evaluation of if statements with await?C# 是否使用 await 对 if 语句执行短路评估? 【发布时间】:2020-12-28 17:48:32 【问题描述】:

我相信一旦 C# 能够判断结果,它就会停止评估 if 语句条件。比如:

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

由于条件(1 &lt; 0) 的计算结果为false,因此无法满足&amp;&amp; 条件,并且不会调用check_something_else()

C# 如何评估带有异步函数的 if 语句?是不是要等两个人都回来?比如:

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

这会短路吗?

【问题讨论】:

ifawait 都不影响短路。 async 不会影响语言的行为,除了允许使用 await ,等待已经执行的异步操作而不会阻塞。 Operator &amp;&amp; 文档的哪一部分让您认为它可以永远跳过短路? @IanKemp:我认为你需要重新阅读 Alexei 所说的话...... 但是,短路和布尔逻辑是不一样的。 ( && false) 也只能计算为 false,但 C、C#、C++ 已决定短路仅基于第一个参数。 短路与if 无关。 &amp;&amp;|| 无论在哪里使用都会执行短路,例如some_var = &lt;expression1&gt; &amp;&amp; &lt;expression2&gt; 【参考方案1】:

是的,它会短路。您的代码相当于:

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 而与最终结果实际上无关的任务引发的任何异常发生什么。)

【讨论】:

如果第一次调用返回false异常将被有效地吞没的明确声明。如果有人使用Task.WhenAny,则通常会忽略这一点。 谢谢,这非常有用。我实际上并不想同时评估它们,我只是好奇await 是如何工作的。 请注意,如果您想实现对所有内容的评估,可以通过使用&amp;而不是&amp;&amp;来更轻松地实现:if (await first_check() &amp; await second_check()) ... 这是因为&amp;运算符不会绕过任何东西,而&amp;&amp; 在结果明确且不能被后续操作数更改时停止(即,如果第一个操作数已经是false,则检查第二个操作数没有意义)。 |||(逻辑 OR 与快捷方式 OR)也是如此,但这里的快捷方式意味着当第一个操作数为 true 时评估停止。【参考方案2】:

检查起来超级简单。

试试这个代码:

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”而不是别的。

【讨论】:

测试表明 C#允许这种短路(假设使用的编译器是兼容的)。它不显示短路是否必需,即程序员是否可以依赖。 我猜这里的一些上下文是两个await 直接一个接一个放置的语句基本上是按顺序执行的。即使它们用于异步编程,awaits 本身的执行顺序也不会改变。 (如果它们在不同的函数或其他东西中,这显然不成立。)因此它总是会在考虑调用second_check() 之前得到first_check() 的结果。因此,短路评估总是以相同的顺序执行,而不是先评估second_check() @Panzercrisis 另一种思考方式是await 的全部意义在于它使异步函数看起来是同步的。当它们在布尔表达式中使用时,没有理由会有所不同。【参考方案3】:

是的。您可以使用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 的第二个示例。

【讨论】:

【参考方案4】:

由于我自己一直在编写编译器,我觉得有资格提供一个更符合逻辑的意见,而不仅仅是基于一些测试。

如今,大多数编译器将源代码转换为 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 本身和我用来表示它的语法完全是即时发明的,所以我希望它很清楚。看起来 *** 标记引擎喜欢它,因为它看起来不错! :)

此时,要弄清楚的是解释的方式。好吧,我可以告诉大多数解释器只是分层评估表达式。因此,它将以这种方式完成:

    计算表达式await first_check() &amp;&amp; await second_check()

      计算表达式await first_check()

        计算表达式first_check()

          解析符号first_check

            是参考吗?否(否则检查它是否引用了委托。) 是方法名吗?是的(我不包括解析嵌套范围、检查它是否是静态的等内容,因为它是题外话,并且问题中没有提供足够的信息来深入挖掘这些细节。)

          评估参数。没有人。因此,将调用名称为first_check 的无参数方法。

          调用名为first_check 的无参数方法,其结果将是表达式first_check() 的值。

        该值应为Task&lt;T&gt;ValueTask&lt;T&gt;,因为这是一个等待表达式。

        await 表达式正在等待获取它最终会产生的值。

      and 表达式的第一个操作数是否产生false?是的。无需计算第二个操作数。

      此时,我们知道await first_check() &amp;&amp; await second_check() 的值也必然是false

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

整个事情的本质是 C# 不会关心表达式是否被等待 - 它仍然是 and 表达式的第一个操作数,因此它将首先被评估。然后,只有当它会产生true 时,才会计算第二个操作数。否则,整个 and 表达式被假定为false,否则不能。

这主要是绝大多数编译器的方式,包括 Roslyn(实际的 C# 编译器,完全使用 C# 编写)和解释器都可以工作,尽管我隐藏了一些无关紧要的实现细节,比如方式await 表达式确实在等待,您可以通过查看生成的字节码来了解自己(您可以使用像 this 这样的网站。无论如何我不隶属于这个网站 - 我只是建议它,因为它使用 Roslyn,我认为这是一个值得记住的好工具。)

澄清一下,await 表达式的工作方式相当复杂,不适合这个问题的主题。它应该得到一个完整的、单独的答案来正确解释,但我认为它并不重要,因为它纯粹是一个实现细节,不会使等待表达式的行为与普通表达式有任何不同。

【讨论】:

我对刚刚收到的反对票很好奇。亲爱的投反对票,您介意评论我的回答以说明投反对票的原因吗?

以上是关于C# 是不是使用 await 对 if 语句执行短路评估?的主要内容,如果未能解决你的问题,请参考以下文章

NodeJS - 在 Array.map 中使用 Async/Await 和“if”语句

C# Async Await 注意事项

C# 异步编程

将wordpress php短代码挂钩放在if语句中是不是正确,其中if语句是post方法的响应?

C# 异步操作 async await 的用法

if else 语句使用包含多个特殊字符的 char[] C#