Taking another break from the iterator series, this week we’ll take a look at an exciting .NET feature that can easily and cleanly remove the calls to a function throughout the whole code base. Unity uses this for Debug.Assert and you can use it for all sorts of functions, too. Wouldn’t it be nice if we could strip out all the debug functions from the production build of our game but leave them in during development? Read on to learn how!

Let’s say our game has a logging class:

public class Logger
{
	public void Debug(string message)
	{
		UnityEngine.Debug.Log(message);
	}
 
	public void Error(string message)
	{
		UnityEngine.Debug.LogError(message);
	}
}
 
void Foo()
{
	var logger = new Logger();
	logger.Debug("debug message");
	logger.Error("error message");
}

Now we want to shut off the debug logs in our production build to improve performance and not leak implementation details. How do we do this? One approach is to go into the Debug function and make it do nothing instead of logging:

public void Debug(string message)
{
	#if DISABLE_DEBUG_LOGGING
		// Logging is disabled so don't do anything
	#else
		// Logging is enabled so log the message
		UnityEngine.Debug.Log(message);
	#endif
}

Then we disable debug logging by going to Edit > Project Settings > Player then adding DISABLE_DEBUG_LOGGING to the Scripting Define Symbols section of the Inspector pane:

Inspector Pane with Scripting Define Symbols

The debug log won’t be printed, but this didn’t fully shut off the debug logging. All the calls to logger.Debug are still there taking up space in the binary, taking up cycles of the CPU, and, perhaps most importantly, having all of their parameters evaluated. Consider a function call like this:

logger.Debug(
	DateTime.Now
	+ " - User "
	+ user.Id
	+ " sent us a message object: "
	+ JsonUtility.ToJson(messageObj)
);

We’re still getting the DateTime.Now value, concatenating a bunch of strings, and serializing an object to JSON. That’s a lot of expensive work considering that the Debug function is just going to ignore it!

Much better would be if we could eliminate the whole call to logger.Debug in the first place. One first pass would be to use another #if like this:

#if DISABLE_DEBUG_LOGGING
#else
	logger.Debug(
		DateTime.Now
		+ " - User "
		+ user.Id
		+ " sent us a message object: "
		+ JsonUtility.ToJson(messageObj)
	);
#endif

But there are four main issues with this approach. First, our code is now ugly. Second, we need to remember to do this every time we call the function. Third, we need to make sure we use the exact same “scripting define symbol” (a.k.a. preprocessor definition) for every function call. Fourth, all of these strings will bloat up our game’s size and be available for anyone who wants to comb through the binary looking for vulnerabilities.

Enter the [Conditional] attribute in System.Diagnostics. Yes, it works in Unity’s old Mono implementation and works like a charm. Here’s how we can change the function:

using System.Diagnostics;
 
[Conditional("ENABLE_DEBUG_LOGGING")]
public void Debug(string message)
{
	UnityEngine.Debug.Log(message);
}
 
logger.Debug("debug message");

Now the call to logger.Debug and all of its parameters will not be compiled if ENABLE_DEBUG_LOGGING isn’t defined. Unfortunately, there’s no way to specify that calls to the function should exist only if a symbol isn’t defined. But we can work around that quite easily:

#if DISABLE_DEBUG_LOGGING
[Conditional("__NEVER_DEFINED__")]
#endif
public void Debug(string message)
{
	UnityEngine.Debug.Log(message);
}

If the DISABLE_DEBUG_LOGGING symbol is defined then we include the [Conditional] attribute, otherwise we don’t and calls to the function are always made. The “condition” isn’t much of a condition though: we only allow calls to the function if the __NEVER_DEFINED__ symbol is defined. Since that should never be defined, calls to the function are effectively disabled.

To finish things up, let’s look at a couple of extensions of this idea. First, can we put a [Conditional] attribute on an interface function? If so, do we need to put it on the class that implements the interface?

public interface ILogger
{
	#if DISABLE_DEBUG_LOGGING
	[Conditional("__NEVER_DEFINED__")]
	#endif
	void Debug(string message);
 
	#if DISABLE_ERROR_LOGGING
	[Conditional("__NEVER_DEFINED__")]
	#endif
	void Error(string message);
}
 
public class Logger : ILogger
{
	// Need [Conditional]?
	public void Debug(string message)
	{
		UnityEngine.Debug.Log(message);
	}
 
	// Need [Conditional]?
	public void Error(string message)
	{
		UnityEngine.Debug.LogError(message);
	}
}

Unfortunately the answer is “no”. You’ll get a compiler error for putting [Conditional] on an interface function. On the plus side, you are allowed to put it on an abstract class! You also don’t have to add it to the concrete (i.e. non-abstract) class that derivies from it. That means this “final” version works out just fine:

public abstract class AbstractLogger
{
	#if DISABLE_DEBUG_LOGGING
	[Conditional("__NEVER_DEFINED__")]
	#endif
	abstract public void Debug(string message);
 
	#if DISABLE_ERROR_LOGGING
	[Conditional("__NEVER_DEFINED__")]
	#endif
	abstract public void Error(string message);
}
 
public class Logger : AbstractLogger
{
	override public void Debug(string message)
	{
		UnityEngine.Debug.Log(message);
	}
 
	override public void Error(string message)
	{
		UnityEngine.Debug.LogError(message);
	}
}
 
void Foo()
{
	var logger = new Logger();
	logger.Debug("debug message"); // unless DISABLE_DEBUG_LOGGING
	logger.Error("error message"); // unless DISABLE_ERROR_LOGGING
}

We now have an easy way to “compile out” all of the calls to a function across the whole code base. We only need to specify the symbol in one place, all the parameters are removed and not evaluated, we save executable size, and the code is just as clean as it was before.

I hope you find this useful in your projects. Let me know in the comments if you use this or a similar technique and how it’s worked out for you!