Skip to main content

C# Preprocessor Directives

Preprocessor directives are special instructions to the C# compiler that control how your code is compiled. Unlike regular C# code that gets executed at runtime, preprocessor directives are processed during compilation, before the actual compilation begins. These directives provide powerful tools for conditional compilation, debugging support, and other compile-time features.

Introduction to Preprocessor Directives

In C#, preprocessor directives always begin with the # symbol and are not terminated with semicolons. They must be written on a single line or continued with the \ character at the end of the line.

Preprocessor directives are primarily used for:

  • Conditional compilation of code blocks
  • Generating warnings and errors at compile-time
  • Organizing and managing source code
  • Assisting in debugging processes

Common C# Preprocessor Directives

Let's explore the most frequently used preprocessor directives in C#:

1. #define and #undef

The #define directive defines a symbol that can be used for conditional compilation, while #undef removes a previously defined symbol.

csharp
#define DEBUG
#define RELEASE_VERSION

// Later in the code
#undef DEBUG

Important notes:

  • #define directives must appear at the beginning of the file, before any code (including using statements)
  • Symbols defined with #define exist only during compilation
  • These are different from constants or variables

2. #if, #else, #elif, and #endif

These directives allow you to conditionally compile sections of code based on whether symbols are defined.

csharp
#define DEMO_VERSION

class Program
{
static void Main()
{
#if DEMO_VERSION
Console.WriteLine("This is the demo version with limited features.");
ShowDemoFeatures();
#elif TRIAL_VERSION
Console.WriteLine("Trial version: Valid for 30 days.");
ShowTrialFeatures();
#else
Console.WriteLine("Full version with all features enabled.");
ShowAllFeatures();
#endif
}
}

In this example, only the code under the #if DEMO_VERSION block will be compiled because we've defined the DEMO_VERSION symbol.

3. #warning and #error

These directives allow you to generate custom compiler warnings or errors.

csharp
#if DEBUG && RELEASE
#error "DEBUG and RELEASE cannot be defined simultaneously!"
#endif

#if DEMO_VERSION
#warning "You are compiling the demo version with limited functionality."
#endif

Output when both DEBUG and RELEASE are defined:

Error CS1029: #error: 'DEBUG and RELEASE cannot be defined simultaneously!'

4. #region and #endregion

These directives let you specify blocks of code that can be expanded or collapsed in the Visual Studio editor.

csharp
class Customer
{
#region Properties

public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Phone { get; set; }

#endregion

#region Methods

public bool Validate()
{
// Validation logic
return true;
}

public void Save()
{
// Save logic
}

#endregion
}

#region directives don't affect the compiled code but help organize and manage source code in the IDE.

5. #pragma

The #pragma directive provides additional instructions to the compiler about specific warnings.

csharp
// Disable warning CS1591 (missing XML documentation)
#pragma warning disable 1591
public class UndocumentedClass
{
public void UndocumentedMethod() { }
}
#pragma warning restore 1591

6. #line

The #line directive changes the line number and file name reported by the compiler for errors and warnings.

csharp
#line 200 "CustomFileName.cs"
int x = y; // If y is undefined, error will report at line 200 in CustomFileName.cs

#line default // Reset to actual line numbering

This is rarely used in everyday development but can be helpful in code generators.

Practical Examples

Example 1: Debug-Only Code

A common use-case is including diagnostic code only when debugging:

csharp
public class DataProcessor
{
public void ProcessData(List<int> data)
{
#if DEBUG
Console.WriteLine($"Processing {data.Count} items");
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
#endif

// Actual processing code
for (int i = 0; i < data.Count; i++)
{
data[i] = data[i] * 2;
}

#if DEBUG
stopwatch.Stop();
Console.WriteLine($"Processing completed in {stopwatch.ElapsedMilliseconds}ms");
#endif
}
}

When compiled in Debug mode, the timing code will be included. In Release mode, it will be removed.

Example 2: Platform-Specific Code

You can use preprocessor directives to handle platform-specific implementations:

csharp
public string GetPlatformSpecificPath(string filename)
{
#if WINDOWS
return $"C:\\Users\\Documents\\{filename}";
#elif LINUX
return $"/home/user/documents/{filename}";
#elif MACOS
return $"/Users/user/Documents/{filename}";
#else
throw new PlatformNotSupportedException("Current platform is not supported.");
#endif
}

To compile for a specific platform, you would define the corresponding symbol:

dotnet build -c Release /p:DefineConstants="WINDOWS"

Example 3: Feature Flags for Different Product Versions

Preprocessor directives can be used to create different product versions:

csharp
public class ProductFeatures
{
public bool IsFeatureAvailable(string featureName)
{
#if ENTERPRISE_EDITION
// All features available
return true;
#elif PROFESSIONAL_EDITION
// Limited premium features
return featureName != "AdvancedAnalytics" &&
featureName != "AutomatedBackups";
#else
// Basic features only
return featureName == "BasicEditing" ||
featureName == "SimpleExport";
#endif
}
}

Conditional Symbols in Visual Studio

You can define conditional compilation symbols in Visual Studio through:

  1. Project properties → Build tab → Conditional compilation symbols field
  2. MSBuild properties in your .csproj file:
xml
<PropertyGroup>
<DefineConstants>DEBUG;TRACE;PREMIUM_FEATURES</DefineConstants>
</PropertyGroup>

Best Practices

  1. Use Sparingly: Excessive use of preprocessor directives can make code harder to read and maintain.

  2. Prefer Other Techniques When Possible:

    • Use configuration files for settings that might change
    • Use dependency injection for platform-specific implementations
    • Use feature flags for toggling features (when they need to be runtime-configurable)
  3. Document Conditional Compilation: Comment your code to explain why certain blocks are conditionally compiled.

  4. Consistent Naming: Use clear, consistent naming for your conditional compilation symbols.

  5. Test All Configurations: Ensure you test your code with different combinations of defined symbols.

Summary

C# preprocessor directives provide powerful tools for controlling the compilation process. They allow for:

  • Conditional compilation for different environments, platforms, or product editions
  • Adding development-time debugging that doesn't affect release builds
  • Organizing code in the IDE
  • Generating compile-time warnings and errors

While powerful, they should be used judiciously to maintain code clarity and maintainability.

Exercises

  1. Create a console application that uses #if DEBUG directives to display additional diagnostic information when run in Debug mode.

  2. Implement a class with methods for file handling that use conditional compilation to use different implementations on Windows vs. Linux.

  3. Create a program that generates different compiler warnings based on defined symbols.

  4. Experiment with #region directives to organize a complex class with many methods and properties.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)