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?
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.
Operator &&
documentation made you think it can ever skip short-circuiting?
if
. &&
and ||
perform short-circuiting no matter where they're used, e.g. some_var = <expression1> && <expression2>
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.)
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.
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 await
s 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.
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.
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.
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:
Evaluate the expression await first_check() && await second_check() Evaluate the expression await first_check() Evaluate the expression first_check() Resolve the symbol first_check Is it a reference? No (otherwise check whether it references a delegate.) 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.) Evaluate arguments. There's no one. So, a parameterless method with the name first_check is to be called. Invoke a parameterless method named first_check and its result will be the value the expression first_check(). The value is expected to be a Task
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.
Success story sharing
false
. This point is often missed if anyone usesTask.WhenAny
.await
works.&
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 alreadyfalse
, 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 istrue
.