Does C# perform short circuit evaluation of if statements with await?
Asked Answered
A

4

76

I believe that C# stops evaluating an if statement condition as soon as it is able to tell the outcome. So for example:

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

Since the condition (1 < 0) evaluates as false, the && condition cannot be met, and check_something_else() will not be called.

How does C# evaluate an if statement with asynchronous functions? Does it wait for both to return? So for example:

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

Will this ever be short-circuited?

Alberthaalberti answered 10/9, 2020 at 8:51 Comment(7)
Neither if nor await affect short-circuiting. async don't affect the behavior of the language apart from allowing await to be used, to await already executing asynchronous operations without blocking.Romeo
Which part of Operator && documentation made you think it can ever skip short-circuiting?Winepress
@AlexeiLevenkov By boolean logic, (false && <anything>) can only ever evaluate to false, hence <anything> does not need to be evaluated. Similarly, (true || <anything>) can only ever evaluate to true, and again <anything> can be skipped.Cloris
@IanKemp: I think you need to re-read what Alexei said...Enact
However, short-circuiting and boolean logic are not the same. (<anyThing> && false) can also only evaluate to false, but C, C#, C++ have decided that short-circuiting is only based on the first argument.Kore
Short circuiting is unrelated to if. && and || perform short-circuiting no matter where they're used, e.g. some_var = <expression1> && <expression2>Des
@PanagiotisKanavos - Could you please help me out by reopening #63866960 ?Hyperspace
H
68

This is super simple to check.

Try this code:

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

It outputs "Tested" and nothing else.

Hyperspace answered 10/9, 2020 at 8:57 Comment(3)
The test demonstrates that this short-circuiting is allowed by C# (assuming the compiler used is compliant). It does not show whether the short-circuiting is required, i.e., can be relied on by the programmer.Ultraconservative
I guess some of the context here is that two await statements that are placed directly one after the other are essentially executed in order. Even though they're used for asynchronous programming, the order in which the awaits themselves are executed never changes. (This obviously doesn't hold if they're in different functions or something.) Therefore it'll always get the result of first_check() back before it even thinks about calling second_check(). So then the short-circuit evaluation is always performed in that same order, without ever evaluating second_check() first.Explode
@Explode The other way to think about it is that the whole point of await is that it makes async functions appear to be synchronous. There's no reason why this should be different when they're used in boolean expressions.Des
B
91

Yes, it will be short-circuited. Your code is equivalent to:

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

Note how it won't even call second_check until the awaitable returned by first_check has completed. So note that this won't execute the two checks in parallel. If you wanted to do that, you could use:

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

if (await t1 && await t2)
{
}

At that point:

  • The two checks will execute in parallel (assuming they're genuinely asynchronous)
  • It will wait for the first check to complete, and then only wait for the second check to complete if the first returns true
  • If the first check returns false but the second check fails with an exception, the exception will effectively be swallowed
  • If the second check returns false really quickly but the first check takes a long time, the overall operation will take a long time because it waits for the first check to complete first

If you want to execute checks in parallel, finishing as soon as any of them returns false, you'd probably want to write some general purpose code for that, collecting the tasks to start with and then using Task.WhenAny repeatedly. (You should also consider what you want to happen to any exceptions thrown by tasks that are effectively irrelevant to the end result due to another task returning false.)

Balkhash answered 10/9, 2020 at 8:56 Comment(3)
Tanks for the explicit statement here that the exception will effectively be swallowed if the first call returns false. This point is often missed if anyone uses Task.WhenAny.Eraste
Thank you, that was super-useful. I don't actually want to evaluate them in parallel, I was just curious about how await works.Alberthaalberti
Note that if you want to achieve evaluating everything, it can be achieved easier by using & instead of &&: if (await first_check() & await second_check()) { ... } This is because the & operator will not bypass anything, while && stops if the result is clear and cannot change by subsequent operands (i.e. if the first operand is already false, then there is no point checking the second operand). The same is the case with | and || (logical OR vs shortcut OR) but here the shortcut means that evaluation stops when the first operand is true.Sussex
H
68

This is super simple to check.

Try this code:

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

It outputs "Tested" and nothing else.

Hyperspace answered 10/9, 2020 at 8:57 Comment(3)
The test demonstrates that this short-circuiting is allowed by C# (assuming the compiler used is compliant). It does not show whether the short-circuiting is required, i.e., can be relied on by the programmer.Ultraconservative
I guess some of the context here is that two await statements that are placed directly one after the other are essentially executed in order. Even though they're used for asynchronous programming, the order in which the awaits themselves are executed never changes. (This obviously doesn't hold if they're in different functions or something.) Therefore it'll always get the result of first_check() back before it even thinks about calling second_check(). So then the short-circuit evaluation is always performed in that same order, without ever evaluating second_check() first.Explode
@Explode The other way to think about it is that the whole point of await is that it makes async functions appear to be synchronous. There's no reason why this should be different when they're used in boolean expressions.Des
S
11

Yes it does. You can check it yourself using sharplab.io, the following:

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

Is effectively transformed by the compiler into something like:

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();
...

Or as a simple program:

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

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

Second example on sharplab.io.

Smallminded answered 10/9, 2020 at 8:57 Comment(0)
L
3

Since I've been writing compilers myself, I feel qualified to offer a more logic opinion that is not merely based on some tests.

Today, most compilers turn source code into an AST (Abstract Syntax Tree), which is used to represent source code a language–independent way.
AST usually consists of syntax nodes. A syntax node that produces a value is called an expression, while one that doesn't produce anything is a statement.

Given the code in the question,

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

let's consider the test condition expression, that is

await first_check() && await second_check()

The produced AST for such code will be something like:

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

The AST itself and the syntax I used to represent it are completely invented on the fly, so I hope it's clear. It looks like StackOverflow markup engine likes it, as it looks nice! :)

At this point, what is to be figured out is the way that'll be interpreted. Well, I can tell most interpreters just evaluate expressions hierarchically. Therefore it will be done pretty much this way:

  1. Evaluate the expression await first_check() && await second_check()

    1. Evaluate the expression await first_check()

      1. Evaluate the expression first_check()

        1. Resolve the symbol first_check

          1. Is it a reference? No (otherwise check whether it references a delegate.)
          2. Is it a method name? Yes (I don't include things like resolving nested scopes, checking if it's static or not, etc. as it's off–topic and not enough information is provided in the question to dig deeper in these details.)
        2. Evaluate arguments. There's no one. So, a parameterless method with the name first_check is to be called.

        3. Invoke a parameterless method named first_check and its result will be the value the expression first_check().

      2. The value is expected to be a Task<T> or ValueTask<T>, since this is an await expression.

      3. The await expression is being waited for to get the value it will eventually produce.

    2. Does the first operand of the and expression produce false? Yes. Needless to evaluate the second operand.

    3. At this point, we know the value of await first_check() && await second_check() will necessarily be false as well.

Some of the checks I've included are done statically (i.e. at compile–time.) However, they're there to make things clearer — needless to talk about compilation, as we're just looking at the way expressions are being evaluated.

The nitty–gritty of this whole thing is that C# won't care whether the expression is awaited or not — it's still the first operand of an and expression, and as such it will be evaluated first. Then, only if it will produce true the second operand is going to be evaluated. Otherwise, the whole and expression is assumed to be false, as it can't be otherwise.

This is mostly the way the vast majority of compilers, including Roslyn (the actual C# compiler, entirely written using C#), and interpreters will work, though I've hidden some implementation details that don't matter, like the way await expression are really waited for, which you can understand yourself by looking at the generated bytecode (you may use a website like this. I'm not anyhow affiliated to this site – I'm just suggesting it because it uses Roslyn and I think it's a nice tool to keep in mind.)

Just to clarify, the way await expressions work is rather complicated and it doesn't fit in the topic of this question. It would deserve a whole, separated answer to be correctly explained, but I don't consider it as important because it's purely an implementation detail and won't make awaited expression behave anyhow differently from normal expressions.

Lazo answered 15/9, 2020 at 20:21 Comment(1)
I'm very curious about the downvote I just received. Dear downvoter, would you mind commenting on my answer to give a reason why the downvote?Lazo

© 2022 - 2024 — McMap. All rights reserved.