Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does the C# compiler remove a chain of method calls when the last one is conditional?

Consider the following classes:

public class A {     public B GetB() {         Console.WriteLine("GetB");         return new B();     } }  public class B {     [System.Diagnostics.Conditional("DEBUG")]     public void Hello() {         Console.WriteLine("Hello");     } } 

Now, if we were to call the methods this way:

var a = new A(); var b = a.GetB(); b.Hello(); 

In a release build (i.e. no DEBUG flag), we would only see GetB printed on the console, as the call to Hello() would be omitted by the compiler. In a debug build, both prints would appear.

Now let's chain the method calls:

a.GetB().Hello(); 

The behavior in a debug build is unchanged; however, we get a different result if the flag isn't set: both calls are omitted and no prints appear on the console. A quick look at IL shows that the whole line wasn't compiled.

According to the latest ECMA standard for C# (ECMA-334, i.e. C# 5.0), the expected behavior when the Conditional attribute is placed on the method is the following (emphasis mine):

A call to a conditional method is included if one or more of its associated conditional compilation symbols is defined at the point of call, otherwise the call is omitted. (§22.5.3)

This doesn't seem to indicate that the entire chain should be ignored, hence my question. That being said, the C# 6.0 draft spec from Microsoft offers a bit more detail:

If the symbol is defined, the call is included; otherwise, the call (including evaluation of the receiver and parameters of the call) is omitted.

The fact that parameters of the call aren't evaluated is well-documented since it's one of the reasons people use this feature rather than #if directives in the function body. The part about "evaluation of the receiver", however, is new - I can't seem to find it elsewhere, and it does seem to explain the above behavior.

In light of this, my question is: what's the rationale behind the C# compiler not evaluating a.GetB() in this situation? Should it really behave differently based on whether the receiver of the conditional call is stored in a temporary variable or not?

like image 451
Kyrio Avatar asked Mar 13 '18 10:03

Kyrio


1 Answers

It comes down to the phrase:

(including evaluation of the receiver and parameters of the call) is omitted.

In the expression:

a.GetB().Hello(); 

the "evaluation of the receiver" is: a.GetB(). So: that is omitted as per the specification, and is a useful trick allowing [Conditional] to avoid overhead for things that aren't used. When you put it into a local:

var b = a.GetB(); b.Hello(); 

then the "evaluation of the receiver" is just the local b, but the original var b = a.GetB(); is still evaluated (even if the local b ends up getting removed).

This can have unintended consequences, so: use [Conditional] with great care. But the reasons are so that things like logging and debugging can be trivially added and removed. Note that parameters can also be problematic if treated naively:

LogStatus("added: " + engine.DoImportantStuff()); 

and:

var count = engine.DoImportantStuff(); LogStatus("added: " + count); 

can be very different if LogStatus is marked [Conditional] - with the result that your actual "important stuff" didn't get done.

like image 62
Marc Gravell Avatar answered Sep 22 '22 22:09

Marc Gravell