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<T>
or ValueTask<T>
, since this is an await expression.
The await expression is being waited for to get the value it will eventually produce.
Does the first operand of the and expression produce false
? Yes. Needless to evaluate the second operand.
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.
if
norawait
affect short-circuiting.async
don't affect the behavior of the language apart from allowingawait
to be used, to await already executing asynchronous operations without blocking. – RomeoOperator &&
documentation made you think it can ever skip short-circuiting? – Winepress(false && <anything>)
can only ever evaluate tofalse
, hence<anything>
does not need to be evaluated. Similarly,(true || <anything>)
can only ever evaluate totrue
, and again<anything>
can be skipped. – Clorisif
.&&
and||
perform short-circuiting no matter where they're used, e.g.some_var = <expression1> && <expression2>
– Des