Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Eliminating #if Preprocessor Directives

4.67/5 (7 votes)
17 Jul 2014CPOL5 min read 22.2K  
Maybe it’s just me, maybe I’m old school, or maybe I’m not old school enough

In my opinion, using the #if preprocessor directive should be a last resort, not a first choice. Consider the scenario where you are working on a project as part of a team. You create a class with a method that does something useful… and then people start coding against it. Of course, they trust that you’ve provided a stable implementation and they don’t need to review your code prior to calling it. So maybe your implementation looks something like this…

C#
static class BestUtilityEver
{
    static void DoSomethingUseful(int id
        #if DEBUG
        , string value
        #endif
        )
    {
        #if DEBUG
        Console.WriteLine("{0:N} = {1}", id, value);
        #else
        Console.WriteLine("{0:N}", id);
        #endif
    }
}

So your colleague came along and started using this new utility and finding it very helpful. His implementation might look something like this…

C#
static class AwesomeProgram
{
    static void Main(string[] args)
    {
        BestUtilityEver.DoSomethingUseful(100, "this is great!");
    }
}

The code compiles and tests out GREAT! It passes through the CI (continuous integration) process just fine, and even makes it through QA without any problem. Then the code is built for release…and that configuration doesn’t define the DEBUG directive. So what happens to the code now?

  • AwesomeProgram.cs: error CS1501: No overload for method ‘DoSomethingUseful’ takes 2 arguments

Now your colleagues’ code has broken the build that everyone thought was solid through QA. That call to DoSomethingUseful should have been written to only pass the second parameter #if DEBUG, but how was your colleague going to know that? There was no commenting that the IDE could recognize to warn the user that the ‘value’ parameter is conditional, the parameter name didn’t indicate that it was only present #if DEBUG, so short of NOT trusting your implementation and reading every method you provide (which the whole team will be doing now) there was no way for anyone to know. All of that embarrassment, trouble, and the resulting mistrust of your code could have been avoided.

Another, even more painful scenario, is one that the compiler cannot catch for you and can very easily make it into production. The over simplified example below shows how misusing preprocessor directives can easily cause you trouble in production, the code compiles perfectly with or without the preprocessor symbol DEBUG being defined. This could easily be a much more important or critical bit of code inside the #if, here it’ll just save us from a null reference exception when DEBUG is defined and introduce a possible null reference exception in production.

C#
using System.Linq;

namespace Blog.PreprocessorDirectives
{
    class Program
    {
        static void Main(string[] args)
        {
            BestUtilityEver.DoSomethingUseful(args.FirstOrDefault());
        }
    }

    public static class BestUtilityEver
    {
        public static void DoSomethingUseful(string id)
        {
            string IdInternal = id;
#if DEBUG
            IdInternal = string.IsNullOrWhiteSpace(IdInternal) ? string.Empty : IdInternal;
#endif
            foreach (var c in IdInternal)
            {
                // Do something fantastic here
            }
        }
    }
}

In this example, the directive simply should not have been used. Perhaps a better example of a possible proper use for #if would be…

C#
#if DEBUG
            IdInternal = string.IsNullOrWhiteSpace(IdInternal) ? "some value": IdInternal;
#else
            if (string.IsNullOrWhiteSpace(IdInternal)) return;
#endif

…at least now we can see the clear benefit in the alternate code. If debug, then make sure we have a string value for some reason, otherwise return early because there’s no reason to continue.

How can you avoid this using a ConditionalAttribute?

C#
using System.Linq;

namespace Blog.PreprocessorDirectives
{
    class Program
    {
        static void Main(string[] args)
        {
            BestUtilityEver.DoSomethingUseful(args.FirstOrDefault());
        }
    }

    public static class BestUtilityEver
    {
        public static void DoSomethingUseful(string id)
        {
            string IdInternal = id;
            DebugFixUpString(ref IdInternal);
            if (string.IsNullOrWhiteSpace(IdInternal)) return;

            foreach (var c in IdInternal)
            {
                // Do something fantastic here
            }
        }

        [System.Diagnostics.Conditional("DEBUG")]
        private static void DebugFixUpString(ref string IdInternal)
        {
            IdInternal = string.IsNullOrWhiteSpace(IdInternal) ? "some value" : IdInternal;
        }
    }
}

With the ConditionalAttribute, we get a few benefits. Depending on your particular situation, you may be able to get other benefits.

  1. All of the code, always exists and always has to compile.
  2. All of the code can be seen by the editor for advanced actions like refactoring and symbol renames.
  3. Even the debug scenario could operate like the release scenario without modifying anything other than the debug method body.

How is this ok for release? Well simply put, the compiler will turn the DebugFixUpString method into a no-op, so that any runtime code which would have called it is now just calling no-op.

Here’s what the reflected code looks like for a release code of the ConditionalAttribute example above.

C#
using System;
using System.Diagnostics;

namespace Blog.PreprocessorDirectives
{
    public static class BestUtilityEver
    {
        [Conditional("DEBUG")]
        private static void DebugFixUpString(ref string IdInternal)
        {
            IdInternal = (string.IsNullOrWhiteSpace(IdInternal) ? "some value" : IdInternal);
        }

        public static void DoSomethingUseful(string id)
        {
            string IdInternal = id;
            if (string.IsNullOrWhiteSpace(IdInternal))
            {
                return;
            }
            string str = IdInternal;
            for (int i = 0; i < str.Length; i++)
            {
                char chr = str[i];
            }
        }
    }
}

You can clearly see that our conditional method still exists, but you can also clearly see that the compiler has removed the call to it because the DEBUG symbol was not defined at compile time.

Another common scenario for preprocessor directives is compatibility. Consider the situation where you’re creating an SDK and providing functionality supporting multiple .NET Framework versions. You might be thinking, this is a place for #if for sure! Not really. We can still avoid #if in multi-version targeting pretty easily.

Using a simple project structure and partial classes, we can target different .NET Frameworks with differing framework functionality and underlying implementation, without affecting the eventual implementation details. Here’s a project structure example where a .NET Framework 4.0 project was created, and then backward compatibility was introduced in a .NET Framework 3.5 project which simply links in all the 4.0 project files and splits the classes with compatibility issues into partial classes to provide alternate implementations for .NET Framework 3.5 targets.

The BestUtilityEver class is now split between three files. One common file in which the implementation is compatible with both framework versions, one v3.5 file for the v3.5 specifics and one v4.0 file with the v4.0 specifics. All three files form one single class at runtime and provide all the required signatures for the consuming implementations. Here’s what those files look like:

C#
namespace Blog.PreprocessorDirectives
{
    public static partial class BestUtilityEver
    {
        public static void DoSomethingUseful(string id)
        {
            string IdInternal = id;
            DebugFixUpString(ref IdInternal);
            if (StringIsNullOrWhiteSpace(IdInternal)) return;

            foreach (var c in IdInternal)
            {
                // Do something fantastic here
            }
        }
    }
}
C#
namespace Blog.PreprocessorDirectives
{
    public static partial class BestUtilityEver
    {
        private static void DebugFixUpString(ref string IdInternal)
        {
            IdInternal = string.IsNullOrEmpty(IdInternal) ? "some value" : IdInternal;
        }

        private static bool StringIsNullOrWhiteSpace(string value)
        {
            return string.IsNullOrEmpty(value) || value.Trim().Length == 0;
        }
    }
}
C#
namespace Blog.PreprocessorDirectives
{
    public static partial class BestUtilityEver
    {
        private static void DebugFixUpString(ref string IdInternal)
        {
            return;
        }

        private static bool StringIsNullOrWhiteSpace(string value)
        {
            return string.IsNullOrEmpty(value) || value.Trim().Length == 0;
        }
    }
}

So, how do you avoid #if in your code?

  • Don’t use preprocessor directives.
    Figure out why you have this variation and try to design it out of your solution, if you can’t try some other mechanism, possibly the ConditionalAttribute or something like it, that the compiler can understand to gracefully handle variations.
  • Know your tools.
    Know what your development tools are capable of, and what your compiler is capable of.
  • Comment your code.
    If you exhaust all other options and have to use preprocessor directives, then comment your code in a way that the IDE your team uses can understand so other team members at least have the chance to understand that they need to pay more attention than usual to this particular implementation. Give your colleagues a fighting chance to not break the build.

While I was writing this, a colleague of mine sent me a link to a case study titled “#ifdef Confirmed Harmful: Promoting Understandable Software Variation” asserting that preprocessor directives are actually harmful to managing software variation. It was a very interesting read, have a look for yourself. What are your thoughts and experiences on preprocessor directives? Am I right, am I wrong, have I just gone completely off the deep end? Like I said in the beginning; maybe it’s just me, maybe I’m old school, or maybe I’m not old school enough, but I want to know what you think.

You’re infinitely more likely to find a better solution if you look for one.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)