Does C# guarantee evaluation order of branched nested expressions?
Asked Answered
D

1

29

C# handles both nested and chained expressions, obviously. If the nesting and/or chaining is linear then it's evident what order the expressions are evaluated in:

Foo(Bar(Baz().Bop())) can only evaluate in the following order:

  • Baz()
  • Bop()
  • Bar()
  • Foo()

But what if the nesting isn't linear? Consider: Foo(Baz()).Bar(Bop())

Clearly the following MUST all be true:

  • Baz before Foo
  • Foo before Bar
  • Bop before Bar

But it's not clear exactly when Bop will be evaluated. Any of the following would be a viable order:

  • Possibility #1
    • Bop()
    • Baz()
    • Foo()
    • Bar()
  • Possibility #2
    • Baz()
    • Bop()
    • Foo()
    • Bar()
  • Possibility #3
    • Baz()
    • Foo()
    • Bop()
    • Bar()

My instinct is that the 3rd option is likely correct. i.e. that it will fully evaluate Foo(Baz()) before it starts to evaluate any of .Bar(Bop())

Whilst I could certainly test an individual situation to see what happens, that doesn't tell me whether my guess will always be true?

But my question is: Is the order of evaluation of branched nested expressions defined as part of the C# language specification, or left to the situational judgement of the compiler?

If not, is it at least known to be deterministic?

Dibru answered 22/4, 2022 at 9:59 Comment(3)
Obvious comment: If your code ever cares about the answer to this question ... then your code is very likely to be poorly written! But that doesn't make the question any less valid or interesting :DDibru
How should Bar ever be called, when Foo did not yet terminate? There is no indstance on which to call Bar, unless Foo has been executed.Pesthole
@Pesthole Read the examples again. It's not about calling Bar before Foo, it's about calling Bop before Foo, or before BazSeurat
S
30

You'll find the answers in Section 11 of the specification.

Specifically, 11.6.6 Function member invocation says:

The run-time processing of a function member invocation consists of the following steps, where M is the function member and, if M is an instance member, E is the instance expression:
...

  • E is evaluated. If this evaluation causes an exception, then no further steps are executed.
  • The argument list is evaluated as described in §11.6.2.

So, given an expression E.M(A), E is fully evaluated before A is evaluated.

For the Foo(Baz()).Bar(Bop()) case, if we're looking at the evaluation of Bar (so E is Foo(Baz()), M is Bar and the argument list is Bop()), this means that Foo (E) must have been fully evaluated before Bop (the argument list) is evaluated, meaning that "possibility #3" is the correct one.

There's also 11.6.2.3 Run-time evaluation of argument lists:

During the run-time processing of a function member invocation (§11.6.6), the expressions or variable references of an argument list are evaluated in order, from left to right

So in the expression M(A, B), A is fully evaluated before B is evaluated.

Seurat answered 22/4, 2022 at 10:18 Comment(3)
Also see devblogs.microsoft.com/oldnewthing/20070814-00/?p=25593 - everything is always left-to-right (unless the precedence of operators states otherwise, e.g. * before + for numerical calculations)Harrow
@MatthewWatson Even so, the order of evaluation is independent of the precedence, and is still always left-to-right. See hereSeurat
Sorry yes, the article I linked actually states that. I should have said "even if the precedence of operators states otherwise".Harrow

© 2022 - 2024 — McMap. All rights reserved.