Conditional compilation in C# - A world apart from the everday ...

A world apart from the everday ...

Assert.IsTrue(Entries.Count == 0);

Conditional compilation in C#

This is a feature which may or may not be new to you. I have often used the

#IF DEBUG
...
#ENDIF

Code block to surround certain bits of code that I only want to run under certain conditions.

But actual conditional methods were fairly new to me until very recently so I thought I'd share my experiences regarding this really cool feature with everybody.

Consider the following code snippet;

using System;

 

namespace ConsoleApplication1

{

class Class1

{

[STAThread]

static void Main(string[] args)

{

int i = 10;

int x = 0;

Class2 worker = new Class2();

Console.WriteLine(worker.DivideNumbers(i, x).ToString());

Console.ReadLine();

}

}

 

class Class2

{

public int DivideNumbers(int num1, int num2)

{

try

{

return num1 / num2;

}

catch(Exception ex)

{

HandleErrorDebugging(ex);

HandleErrorNicely(ex);

}

return 0;

}

 

[System.Diagnostics.Conditional("DEBUG")]

private void HandleErrorDebugging(Exception ex)

{

Console.WriteLine("An error occurred: " + ex.Message + Environment.NewLine + ex.StackTrace);

}

 

private void HandleErrorNicely(Exception ex)

{

Console.WriteLine("Ooops! Something has gone pear! Please contact the care bears as I am sure they can assist you.");

}

}

}

 

What do you suppose will happen to the HandleErrorDebugging method if DEBUG is not defined? Well nothing. You might be surprised by that statement but let me explain ... nothing happens to the method, it is compiled into the code regardless of whether or not the DEBUG switch is set.

 

 

So what is the point you say? Well the difference comes in at the point of calling the method. So let's take a look at the IL code produced with DEBUG set and without DEBUG set ...

 

Firstly, with DEBUG on;

  .try

  {

    IL_0000:  ldarg.1

    IL_0001:  ldarg.2

    IL_0002:  div

    IL_0003:  stloc.1

    IL_0004:  leave.s    IL_001b

  }  // end .try

  catch [mscorlib]System.Exception

  {

    IL_0006:  stloc.0

    IL_0007:  ldarg.0

    IL_0008:  ldloc.0

    IL_0009:  call       instance void ConsoleApplication1.Class2::HandleErrorDebugging(class [mscorlib]System.Exception)

    IL_000e:  ldarg.0

    IL_000f:  ldloc.0

    IL_0010:  call       instance void ConsoleApplication1.Class2::HandleErrorNicely(class [mscorlib]System.Exception)

    IL_0015:  leave.s    IL_0017

  }  // end handler

  IL_0017:  ldc.i4.0

  IL_0018:  stloc.1

  IL_0019:  br.s       IL_001b

  IL_001b:  ldloc.1

  IL_001c:  ret

} // end of method Class2::DivideNumbers

 

And secondly, with DEBUG off;

  .try

  {

    IL_0000:  ldarg.1

    IL_0001:  ldarg.2

    IL_0002:  div

    IL_0003:  stloc.1

    IL_0004:  leave.s    IL_0012

  }  // end .try

  catch [mscorlib]System.Exception

  {

    IL_0006:  stloc.0

    IL_0007:  ldarg.0

    IL_0008:  ldloc.0

    IL_0009:  call       instance void ConsoleApplication1.Class2::HandleErrorNicely(class [mscorlib]System.Exception)

    IL_000e:  leave.s    IL_0010

  }  // end handler

  IL_0010:  ldc.i4.0

  IL_0011:  ret

  IL_0012:  ldloc.1

  IL_0013:  ret

} // end of method Class2::DivideNumbers

 

Looking at the differences shown above you can clearly see that the ONLY difference is that when DEBUG is not defined the call to the conditional method is not included.

Why is this you say?

 

Well what would happen if this conditional method was referenced by another assembly?

If the method was removed by the compiler then this would have an effect on the compilation of the other assembly.

 

So what does happen in this scenario?

 

  1. App built as Debug using Library built as Debug  -->
  2. The HandleDebug & HandleNicely methods are called.
  3. App built as Release using Library built as Debug -->
  4. The HandleDebug method is NOT called.
  5. App built as Debug using Library built as Release -->
  6. Same as scenario 1.
  7. App built as Release using Library built as Release -->
  8. Same as scenario 2.

 

Proof then that it is the calling assembly's compilation method that determines which conditional methods are called. This is made possible by the fact that the C# compiler creates the conditional methods regardless of whether it was run under Debug or as Release.

 

Where this all gets really interesting is when you introduce inheritance into the equation ...

 

Consider the following code snippet;

using System; 
using System.Diagnostics; 
class Class1  
{ 
   [Conditional("DEBUG")] 
   public virtual void M() { 
      Console.WriteLine("Class1.M executed"); 
   } 
} 

using System; 
class Class2: Class1 
{ 
   public override void M() { 
      Console.WriteLine("Class2.M executed"); 
      base.M();       // base.M is not called! 
   } 
} 

#define DEBUG 
using System; 
class Class3 
{ 
   public static void Test() { 
      Class2 c = new Class2(); 
      c.M();              // M is called 
   } 
}
  

Class2 includes a call to the M defined in its base class. This call is omitted because the base method is conditional based on the presence of the symbol DEBUG, which is undefined. Thus, the method writes to the console "Class2.M executed" only.

 

Subtle!

 

Therefore it is important when using conditional statements to REALLY understand when the call is omitted and when it is not, else all sorts of hassles can creep into your code.

 

If you remember one thing about conditional methods it should be this;

 

Inclusion or exclusion of a call to a conditional method is controlled by the conditional compilation symbols AT THE POINT OF THE CALL!

 

That's it from me for now ... happy conditional programming.

Posted: Jul 03 2006, 09:19 AM by Ryan CrawCour | with no comments
Filed under:
Leave a Comment

(required) 

(required) 

(optional)

(required) 


Enter the numbers above: