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?

Brondahl
  • 7,402
  • 5
  • 45
  • 74
  • 18
    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 :D – Brondahl Apr 22 '22 at 09:59
  • 1
    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. – MakePeaceGreatAgain Apr 22 '22 at 10:25
  • 1
    @MakePeaceGreatAgain Read the examples again. It's not about calling `Bar` before `Foo`, it's about calling `Bop` before `Foo`, or before `Baz` – canton7 Apr 22 '22 at 10:26

1 Answers1

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.

canton7
  • 37,633
  • 3
  • 64
  • 77
  • 2
    Also see https://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) – Matthew Watson Apr 22 '22 at 10:20
  • 4
    @MatthewWatson Even so, the order of evaluation is independent of the precedence, and is still always left-to-right. [See here](https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AXEUCuA7AHwAEAmARgFgAoaosgTgAoBBRgSgAIBqDgIXY4AqDgGF2bANzVaZJBwCWeDB1acA3hzpMARM22TNAdg5kJHAL4y5i5f3WaGjbb31mixkmctU61paIENLScRVyMOAGYvIA=) – canton7 Apr 22 '22 at 10:25
  • 1
    Sorry yes, the article I linked actually states that. I should have said "even if the precedence of operators states otherwise". – Matthew Watson Apr 22 '22 at 10:50