The C# Preprocessor Directives
Besides the usual keywords, most of which you
have now encountered, C# also includes a number of commands that
are known as preprocessor directives. These
commands never actually get translated to any commands in your
executable code, but instead they affect aspects of the compilation
process. For example, you can use preprocessor directives to
prevent the compiler from compiling certain portions of your code.
You might do this if you are planning to release two versions of
the code - a basic version and an enterprise version that will have
more features. You could use preprocessor directives to prevent the
compiler from compiling code related to the additional features
when you are compiling the basic version of the software. Another
scenario is that you might have written bits of code that are
intended to provide you with debugging information. You probably
don’t want those portions of code compiled when you actually ship
the software.
The preprocessor directives are all distinguished
by beginning with the # symbol.
|
|
Tip |
C++ developers will recognize the
preprocessor directives as something that plays an important part
in C and C++. However, there aren’t as many preprocessor directives
in C#, and they are not used as often. C# provides other
mechanisms, such as custom attributes, that achieve some of the
same effects as C++ directives. Also, note that C# doesn’t actually
have a separate preprocessor in the way that C++ does. The
so-called preprocessor directives are actually handled by the
compiler. Nevertheless, C# retains the name preprocessor directive
because these commands give the impression of a preprocessor.
|
The next sections briefly cover the purposes of the
preprocessor directives.
#define and #undef
#define is used
like this:
What this does is tell the compiler that a symbol
with the given name (in this case DEBUG)
exists. It is a little bit like declaring a variable, except that
this variable doesn’t really have a value - it just exists. And
this symbol isn’t part of your actual code; it exists only for the
benefit of the compiler, while the compiler is compiling the code,
and has no meaning within the C# code itself.
#undef does the
opposite, and removes the definition of a symbol:
If the symbol doesn’t exist in the first place,
then #undef has no effect. Similarly,
#define has no effect if a symbol
already exists.
You need to place any #define and #undef
directives at the beginning of the C# source file, before any code
that declares any objects to be compiled.
#define isn’t much use
on its own, but when combined with other preprocessor directives,
especially #if, it becomes very
powerful.
|
|
Tip |
Incidentally, you might notice some changes
from the usual C# syntax. Preprocessor directives are not
terminated by semicolons and normally constitute the only command
on a line. That’s because for the preprocessor directives, C#
abandons its usual practice of requiring commands to be separated
by semicolons. If it sees a preprocessor directive, it assumes that
the next command is on the next line.
|
#if, #elif, #else, and #endif
These directives inform the compiler whether
or not to compile a block of code. Consider this method:
This code will compile as normal, except for the
Console.WriteLine() method call that is
contained inside the #if clause. This
line will only be executed if the symbol DEBUG has been defined by a previous #define directive. When the compiler finds the
#if directive, it checks to see if the
symbol concerned exists and only compiles the code inside the
#if clause if the symbol does exist.
Otherwise, the compiler simply ignores all the code until it
reaches the matching #endif directive.
Typical practice is to define the symbol DEBUG while you are debugging and have various bits
of debugging-related code inside #if
clauses. Then, when you are close to shipping, you simply comment
out the #define directive, and all the
debugging code miraculously disappears, the size of the executable
file gets smaller, and your end users don’t get confused by being
shown debugging information. (Obviously, you would do more testing
to make sure your code still works without DEBUG defined.) This technique is very common in C
and C++ programming and is known as conditional
compilation.
The #elif (=else if) and #else
directives can be used in #if blocks and
have the intuitively obvious meanings. It is also possible to nest
#if blocks:
|
|
Tip |
Note that, unlike the situation in C++, using
#if is not the only way to compile code
conditionally. C# provides an alternative mechanism through the
Conditional attribute, which is explored
in Chapter 12, “Reflection.”
|
#if and #elif support a limited range of logical operators
too, using the operators !, ==, !=, and ||. A symbol is considered to be true if it exists and false if it doesn’t. For example:
#warning and #error
Two other very useful preprocessor directives
are #warning and #error. These will respectively cause a warning or
an error to be raised when the compiler encounters them. If the
compiler sees a #warning directive, it
will display whatever text appears after the #warning to the user, after which compilation
continues. If it encounters a #error
directive, it will display the subsequent text to the user as if it
were a compilation error message and then immediately abandon the
compilation, so no IL code will be generated.
You can use these directives as checks that you
haven’t done anything silly with your #define statements; you can also use the
#warning statements to remind yourself
to do something:
#region and #endregion
The #region and
#endregion directives are used to
indicate that a certain block of code is to be treated as a single
block with a given name, like this:
This doesn’t look that useful by itself; it
doesn’t affect the compilation process in any way. However, the
real advantage is that these directives are recognized by some
editors, including the Visual Studio .NET editor. These editors can
use these directives to lay out your code better on the screen. You
see how this works in Chapter 14, “Visual Studio 2009.”
#line
The #line
directive can be used to alter the file name and line number
information that is output by the compiler in warnings and error
messages. You probably won’t want to use this directive that often.
Its main use occurs if you are coding in conjunction with some
other package that alters the code you are typing in before sending
it to the compiler, since this will mean line numbers, or perhaps
the file names reported by the compiler, won’t match up to the line
numbers in the files or the file names you are editing. The
#line directive can be used to restore
the match. You can also use the syntax #line
default to restore the line to the default line
numbering:
#pragma
The #pragma
directive can either suppress or restore specific compiler
warnings. Unlike command-line options, the #pragma directive can be implemented on a class or
method level, allowing a fine-grained control of what warnings are
suppressed and when. The following example disables the field not
used warning and then restores it after the MyClass class compiles.