Professor Mustard Programming
 

Programming 101 - Unit 06 - Other Features

06h) Preprocessor Statements
 
When writing and debugging a program, you may find yourself temporarily changing how the program to operates, in order to make your life easier. Maybe there's a big section of the program you want to skip, so that you can start testing the new part. Maybe you're hard-coding a value that the user would normally enter, because you're testing a specific scenario. Maybe you simply need to work around a method that hasn't been written yet. Whatever the case, life in a program is very different when it's under construction, and it's important to have a good strategy for dealing with it.

One possibility is to simply "comment out" the blocks of code that you don't want to use, by putting "//" in front of the undesirable lines. This is far better than deleting the code outright, because restoring the code to normal is a simple matter of removing the comments. You can actually use /* and */ to "comment out" entire sections of code, without having to add a "//" to each line! However, this would still mean multiple code changes across your program when you want to restore the code to its intended functionality, and what if you forget to bring one piece of code back? Wouldn't it be nicer to just make this process automatic?

Well, this is one of the primary uses of preprocessor statements. Start by defining a DEBUG constant at the very top of your code file (even above the "using" statements):

#define DEBUG


This statement only has one purpose: it tells the rest of your program that there is a constant called "DEBUG", defined in your code. The next step is to write code that uses this constant:

[STAThread]
static void Main(string[] args)
{
#if DEBUG
  Console.WriteLine("We're in debug mode!");
#else
  Console.WriteLine("We're not in debug mode!");
#endif
  PromptForExit();
}



Preprocessor statements can look a little scary, because they start with # and tend to ignore the rest of your program's indentation structure. But you can basically tell what the above code is doing just by reading it out loud, right? If you've defined a DEBUG constant, the first WriteLine() statement is used. Otherwise, the second WriteLine() is used.

Basically, anytime that you want your program to work differently while you're debugging it, write an #if that checks for DEBUG (as always, the name is arbitrary). When you want to start debugging, simply add the "#define DEBUG" line to the top of your code, and your program will automatically start using the code under the #if. "Comment out" the #define, and your program will revert back to using the code under the #else.

Incidentally, these aren't ordinary "if" statements that are evaluated every time your code runs. Preprocessor statements are only evaluated once, when you actually compile your program. The code in the "false" portion of the #if won't even be added to your final executable (hence the term "preprocessor"... it occurs before your program is actually processed)! It's also worth noting that, for Visual Studio users, you don't even need to define a DEBUG constant. When compiling in "Debug" mode, your solution will assume that there's a DEBUG constant, and when compiling in "Release" mode, your solution will assume that there isn't a DEBUG constant.

Also... be careful when writing #if DEBUG statements into your code. They should be used sparingly, because it can produce baffling errors when your program is able to compile in different ways (imagine telling a customer on the phone, "what do you mean, 'it crashed'? It worked fine when I was in debug mode!"). I believe that preprocessor statements are still safer than "commenting out" code in a piecemeal fashion, but even so: a good program design shouldn't need to use these guys in very many places. When I have a program that includes these statements, I make sure to include one that adds the word "DEBUG" to my application's title bar, so that there's never any doubt as to which "version" of my program is currently running.

One other thing. We can use the #region and #endregion tags to group big sections of code together, like so...

#region "Utility Methods"

static void SomeMethod()
{
}

// ...more methods...

#endregion



These preprocessor tags are totally optional, and are most frequently used to "fence off" sections of code that you don't need to think about. You can also place them to logically group parts of your code together. For example, place a set of #region/#endregion tags around all the getters and setters in a class: a professional code editor will often allow you to "hide" all of the code in a certain region, so that there's less to look at. It can be a handy feature, especially in Form-based classses, which tend to get rather big.

The code sample below summarizes the use of #define, #if and #region.



// Note that your "Define" statements must appear at the top of
// your file (comments don't count).

// Also note that, for Visual Studio users, your projects will
// automatically define a DEBUG constant when you are compiling
// in the"Debug" Solution Configuration.
#define DEBUG

using System;

namespace H_Preprocessor
{
  class Class1
  {
    static readonly string ln = Environment.NewLine;

    [STAThread]
    static void Main(string[] args)
    {
      // "#if DEBUG" evaluates to true if a #define DEBUG occurs
      // anywhere in your program. You can "comment out" the define
      // line to quickly toggle in and out of "debug" mode.

      // Note that because these preprocessor #if's revolve around
      // constants, they are only evaluated once, at compile time.
      // The code in the "false" portion of the #if won't even be
      // compiled into your executable!
#if DEBUG
      Console.WriteLine("We're in debug mode!");
#else
      Console.WriteLine("We're not in debug mode!");
#endif
      PromptForExit();
    }

    // "Region" tags can be used to group related methods and code together.
    // They are optional in any event, and you can group methods into regions
    // using any metric you like (purpose, inheritence, public/private, etc).
#region "Utility Methods"
    static void PromptForExit()
    {
      Console.Write(ln + "Program complete! Hit enter to exit...");
      Console.ReadLine();
    }
#endregion

  }
}