C# Preprocessor Directives Tutorial

In this tutorial we learn commands in C#, called preprocessor directives, that affect the compilation process before compilation.

We cover how to define a preprocessor directive and look at different directives such as the region, define, if else and pragma warning directives.

Here's a table of contents of what you'll learn in this lesson:
(click on a link to skip to its section)

Let's jump right in.

What is a preprocessor directive

C# preprocessor directives are commands that affect the compilation process and are processed before compilation. Even though the compiler doesn’t have a separate preprocessor, the directives are processed as if there were one.

C# preprocessor directives cannot be used to define MACROS like in C and C++.

How to define a preprocessor

A preprocessor directive starts with an octothorp (#), immediately followed by the directive.

Syntax:
 #directive DIRECTIVE_NAME

The statement is not terminated by a semicolon and the directive must be the only instruction on a line.

Example:
 #define LOGGING
Example:
#define LOGGING

using System;

namespace PreProcDirectives
{
    class Program
    {
        static void Main()
        {
            #if (LOGGING)
                Console.WriteLine("Logging is enabled");
                Console.ReadLine();
            #endif
        }
    }
}

In the example above, at the very top of the document, we use the #define directive to define a symbol called LOGGING.

Then, we use the symbol in the #if conditional directive to check if it exists. Because it does exist, the code between the #if and #endif gets compiled.

The following table lists the preprocessor directives available in C#:

DirectiveDescription
#ifConditional if
#elseConditional else
#elifConditional else if
#endifEnds the conditional statement
#defineDefine a symbol. Note that this is not a macro or constant.
#undefRemove a symbol.
#warningPrint a warning to the Error List.
#errorPrint an error to the Error List. Note that the compiler will not compile with an error.
#lineModify compiler line number and file name.
#regionStart of a collapsable region.
#endregionEnd of a collapsable region.
#pragmaSpecial custom instructions
#pragma warningSuppress a warning

Let’s look at some of common directives used in applications.

The region and endregion directives

The #region and #endregion directives allow us to collapse a specific region of code in Visual Studio.

Example:
using System;

namespace PreProcDirectives
{
    #region Main Class
    class Program
    {
        static void Main()
        {

        }
    }
    #endregion
}

In the example above, we specify the start of the region with #region that we want to be able to collapse, and give it a short description.

Then, we specify the end of the region with #endregion. Anything in between will be able to collapse when you press the - button next to the line number.

When collapsed it would look like this:

Example:
using System;

namespace PreProcDirectives
{
    Main Class
}

Regions are considered anti-patterns. They require more work which doesn’t increase the quality or readability of the code, reduce the number of bugs, and makes the code more complicated to refactor.

The define and undef directives

The #define and #undef directives allow us to define and undefine symbols for use with conditional directives.

Example:
#define LOGGING
#undef LOGGING

using System;

namespace PreProcDirectives
{
    class Program
    {
        static void Main()
        {
            #if (LOGGING)
                Console.WriteLine("Logging is enabled");
                Console.ReadLine();
            #else
                Console.WriteLine("Logging is disabled");
                Console.ReadLine();
            #endif
        }
    }
}

In the example above, at the very top of the document, we use the #define directive to define a symbol called LOGGING. We can then use the symbol in the #if conditional directive to check if it exists.

Just below the #define, we #undef the directive for demonstration purposes. This means that LOGGING doesn’t exist anymore.

In the #if conditional directive, it can’t find the LOGGING symbol so it moves on to the #else directive.

The if, elif, else and endif directives

The conditional directives work similarly to normal conditional statements. They also have a limited subset of boolean operators: ++, !=, &&, ||, !, ()

Example:
#define LOGGING
#define CODELOGGING

using System;

namespace PreProcDirectives
{
    class Program
    {
        static void Main()
        {
            #if (LOGGING && !CODELOGGING)
                Console.WriteLine("Logging is enabled");
                Console.ReadLine();
            #elif (CODELOGGING && !LOGGING)
                Console.WriteLine("CODE: 1028");
                Console.ReadLine();
            #elif (LOGGING && CODELOGGING)
                Console.WriteLine("CODE: 1032 Full logging enabled");
                Console.ReadLine();
            #else
                Console.WriteLine("Logging is disabled");
                Console.ReadLine();
            #endif
        }
    }
}

In the example above, we #define two symbols, LOGGING and CODELOGGING at the top of the page.

In the Main() function we check which of these exist and print a message to the console.

The pragma warning disable and pragma warning restore directives

The #pragma warning disable directive disables reporting of a warning. It is useful when you know about and understand the warning, but still want to disable it.

Example:
using System;

namespace PreProcDirectives
{
    class Program
    {
        static void Main()
        {
            #pragma warning disable
            if (false)
            {
                Console.WriteLine("Unreachable");
                Console.ReadLine();
            }
            #pragma warning restore
        }
    }
}

In the example above, we create a conditional statement that will always be false, anything in the code block will never be reached. By default, the compiler will warn us of unreachable code.

Then, we use the #pragma warning disable directive at the start of the code that gives a warning, and #pragma warning restore at the end of the code that gives a warning. This directive will suppress the warning.

A real world example

A real world example would be to exclude certain pieces of code based on a condition.

Let’s say you’re creating a game, and would like to deploy that game several platforms such as the PC, Playstation and XBox.

Example:
namespace MyAwesomeGame
{
    class Program
    {
        static void Main() { }
    }

#if(PLAYSTATION)
    // Playstation-specific code here
#elif (XBOX)
    // XBox-specific code here
#else
    // PC-specific code here
#endif
}

An overly simple example and we could, of course, factor out these differences using other means but this is a perfectly valid way of excluding code before compilation.

Summary: Points to remember

  • A preprocessor directive is a command that is preprocessed before compilation.
  • These directives can suppress errors, control the flow of the application and more.