How does C# compiler remove Debug.Assert's in

2019-03-12 00:03发布

问题:

I was recently going through some code and considering whether I need to be careful with the expressions placed inside Debug.Assert statements, such as expensive operations or those with side effects. However, it appears the compiler is pretty smart about completely removing the Assert statement and inner expressions.

For example, the following will only print on debug builds:

static void Main(string[] args)
{
    Debug.Assert(SideEffect());
}
private static bool SideEffect()
{
    Console.WriteLine("Side effect!");
    return true;
}

And this will complain that o is being used before initialization on release builds:

static void Main(string[] args)
{
    object o;
    Debug.Assert(Initialize(out o));
    o.ToString();
}
private static bool Initialize(out object o)
{
    o = new object();
    return true;
}

It even seems to stand up to expressions such as this (printing "After" in both cases):

static void Main(string[] args)
{
    if (false) Debug.Assert(true);
    Console.WriteLine("After");
}

I was a little suprised with how smart the compiler is here and its ability to correctly detect cases when the Debug.Assert is removed. So, it got me curious..

  • How exactly is the statement removed? The expression tree must be built before the statement is removed in order to properly execute the above if statement correctly.
  • Is the System.Diagnostics.Debug class special here, or is it possible to build your own methods with similar handling?
  • Are there any ways to "trick" the preprocessor here? Even better, are there situations that one might encounter in real-world code where this could be problematic?

回答1:

Debug.Assert is declared with ConditionalAttribute; as the documentation states, this "[i]ndicates to compilers that a method call or attribute should be ignored unless a specified conditional compilation symbol is defined."

The C# compiler has specific support for that attribute and removes the Debug.Assert during release builds, so it is never part of the built expression tree.

If you right-click on one of your Debug.Assert statements, you should be able to go to the definition. Visual Studio will show you "code" generated from the metadata, and there you can see the [Conditional("DEBUG")] attribute applied. So this code is only respected when DEBUG is #define'd as part of your build.



回答2:

The methods on the debugger use the pseudo-custom attribute, ConditionalAttribute, which the compiler detects and removes any calls to any methods with that attribute unless the specified compilation symbol (in this case, DEBUG) is defined. Anyone may use the attribute on void methods without any out parameters.



回答3:

I don't believe that Debug.Assert is special in any way; it's just using the Conditional attribute so that the compiler removes it when it detects that the 'preprocessor' define does not exist (C# does not have a preprocessor!).

You can use it like so to do the same thing (so long as you've defined DEBUG (or whatever symbol you want to switch on, TRACE is another popular one):

[Conditional("DEBUG"), Conditional("TRACE")]
public void DebugOnlyMethod() {
    Console.WriteLine("Won't see me unless DEBUG or TRACE is defined");
}