Working with Preprocessor Directives in C#
Overview
C# provides several preprocessor directives that influence the compilation process without generating executable code. While C# lacks a standalone preprocessor like C/C++, these directives are processed by the compiler and serve important purposes in code organization, conidtional compilation, and build configuration.
Preprocessor directives begin with the # symbol. They do not require semicolons for termination and typically occupy single lines. Unlike regular C# statements, these commands must appear before any namespace or class declarations in the file.
Defining and Undefiinng Symbols
The #define directive declares a compilation symbol that exists during the compile phase:
#define TRACE_ENABLED
This creates a symbol named TRACE_ENABLED that the compiler recognizes. Unlike variables, these symbols hold no actual value—they simply exist or don't exist.
The corresponding #undef directive removes a previously defined symbol:
#undef TRACE_ENABLED
If a symbol hasn't been defined, #undef has no effect. Conversely, attempting to #define an already existing symbol does nothing. These directives must appear at the very top of the source file, before any other code statements.
By themselves, these declarations serve no purpose. Their real power emerges when combined with conditional compilation directives.
Conditional Compilation with #if, #elif, #else, and #endif
These directives control whether code blocks get compiled based on defined symbols:
int ProcessValue(double input)
{
int result = CalculateResult(input);
#if DEBUG
Console.WriteLine($"Input: {input}, Result: {result}");
#endif
return result;
}
When the DEBUG symbol is defined, the diagnostic output gets included. When undefined, the compiler ignores the entire block until the matching #endif.
For more complex scenarios, use #elif for additional conditions and #else for fallback cases:
#if ENTERPRISE
EnableAdvancedFeatures();
ConfigureCustomReporting();
#elif STANDARD
EnableBasicFeatures();
#else
ShowLimitedFunctionality();
#endif
You can also nest conditional blocks for more granular control:
#if FEATURE_A
InitializeFeatureA();
#if DETAILED_LOGGING
LogFeatureAActivation();
#endif
#elif FEATURE_B
InitializeFeatureB();
#else
#error No features enabled. Define FEATURE_A or FEATURE_B.
#endif
Generating Compiler Warnings and Errors
The #warning directive emits a warning message during compilation:
#if OBSOLETE
#warning This code path uses deprecated functionality
#endif
The #error directive halts compilation with a specified error message:
#if NET45
#error This library requires .NET Framework 4.5 or later
#endif
These directives prove valuable for enforcing build prerequisites and alerting developers to configuration issues early in the compilation process.
Code Organization with #region and #endregion
These directives enable collapsible code sections in editors, improving readability for large files:
public class DataProcessor
{
#region Initialization
private readonly string connectionString;
private SqlConnection connection;
public DataProcessor(string connStr)
{
connectionString = connStr;
}
#endregion
#region Data Operations
public void ExecuteQuery(string query)
{
// query execution logic
}
public DataTable FetchResults()
{
// data retrieval logic
}
#endregion
#region Cleanup
public void Dispose()
{
connection?.Dispose();
}
#endregion
}
Regions can be nested and span any number of lines. They have no impact on compiled output—purely a developer experience enhancement.
Line Number Control with #line
The #line directive modifies the line numbers and filename reported by the compiler, useful when code is generated or transformed:
#line 42 "GeneratedCode.cs"
var item = new {
Id = 1001,
Name = "Sample"
};
#line default
Error messages and warnings will reference line 42 of "GeneratedCode.cs" rather than the actual source position. The #line default directive restores the default line numbering scheme.
This becomes particularly relevant when working with code generation tools, transpilers, or metaprogramming frameworks where the source line numbers differ from generated output.
Suppressing Warnings with #pragma
The #pragma directive controls compiler warnings at a granular level—either disabling specific warnings or restoring previously disabled ones:
#pragma warning disable 168, 219
public class SampleClass
{
private int unusedField;
private int anotherUnusedField;
}
#pragma warning restore 168, 219
In this example, warnings 168 (unused local variable) and 219 (unused private field) are suppressed for the SampleClass declaration, then restored afterward. This approach provides finer control than command-line warning suppression, allowing targeted silencing within specific code sections.
You can also disable all warnings and restore them selectively:
#pragma warning disable
// Third-party code with known issues
#pragma warning restore 109
Practical Applications
Preprocessor directives enable several common development patterns: creating debug-only logging that disappears in release builds, maintaining single codebases that compile differently for various platforms, marking code sections under development with compiler warnings, and organizing large code files into logical, collapsible sections.
Understanding these directives helps when debugging build issues, working with legacy codebases, or implementing build systems that require conditional compilation logic.