Moving from Delphi to C# is fun most of the time.
Discovering stuff I can do in C# that is impossible, or (very) time consuming, in Delphi is fun. Discovering that stuff I take for granted in Delphi – i.e. metaclasses (officially “class references”), not having to re-declare constructors to use them to instantiate descendant classes – is impossible in C#, is less fun, but still interesting.
Sometimes I am just plain astonished, if not to say flabbergasted.
One such occurrence happened a couple of weeks ago. I was working on some code where I was putting a call to a validation method in a Debug.Assert
to check for contract violations. I naively assumed that C#’s Debug.Assert
would behave in a very similar manner to Assert
in Delphi. And was completely taken off guard by the differences.
Assert in Delphi
In Delphi the Assert
looks as follows. (The message part is optional.)
Assert({boolean expression}False, 'Message used when the boolean expression evaluates to false.');
When you call Assert
in Delphi and the boolean expression evaluates to false
this raises (throws) an EAssertionFailed
exception.
The beauty of an exception being raised is that it enables you to use Assert calls to ensure a method’s contract is met. The exception guarantees that execution won’t proceed beyond the Assert
call when the contract is violated. Barring any except
(catch
) and finally blocks of course.
You can control whether assertions are compiled into an executable using an “Assert directive”
{$C+} // or {$ASSERTIONS ON} // to turn assertions on, and {$C-} // or {$ASSERTIONS OFF} // to turn them off
As the Assert directive has local scope, you can keep some asserts even when all others are turned off. Using the {$IFOPT xxx}
directive you can even create a construct that reinstates the project scope value after your local overrule.
This is the way my world has worked for the good many years that I have developed using Delphi.
Enter C#
And see my world turned on its head.
The Debug
class is where you turn for Assert
‘s.
Or is it?
Surprise 1. Two classes offering Assert
First surprise was that there are two classes you can use for assertions: Debug
and Trace
. The Assert
overloads have the exact same functionality in both classes.
Why?
Apparently Debug.Assert
the calls are automagically removed from assemblies compiled when the DEBUG
conditional is not defined, while Trace.Assert
calls are always compiled into an assembly.
And that was the second surprise.
Surprise 2. DEBUG conditional dictates inclusion and exclusion
The Delphi convention certainly is that you turn assertions on for debug builds and off for release builds, but there is no one holding a gun to your head.
In fact there can be many reasons to include some assertions in release builds, while excluding others.
For one, the code run to evaluate the boolean expression may have side effects that you do not want to miss out on. See Assertions in Managed Code for an example.
For another, some checks are more important than others. While you might be willing to forego the contract checks of most assertions, there are some checks where you want to guarantee that the code protected by that check is never executed if the contract was violated.
The choice in C# certainly seems more “elegant”: use Debug.Assert
if you are ok with excluding the check, use Trace.Assert
if you want to keep it. A lot easier and more readable than using IFDEF
and/or IFOPT
directives.
So, simply use Trace.Assert
for any Assert you want to make it into your assembly regardless of whether you created a DEBUG
or a release build?
Well … No.
Enter surprises number three.
Surprise 3. Asserts in C# bring up dialogs
According to the MSDN documentation, Debug.Assert
:
Checks for a condition; if the condition is false, outputs messages and displays a message box that shows the call stack.
Scuse me? What the bleep?
How about Trace
then? That is set to compile into release builds, so it wouldn’t really bring up a dialog now would it? Well, unfortunately, it does. The documentation for Trace.Assert
is – letter by letter – exactly the same as for Debug.Assert
.
Which means that if you are building servers and services, you’d better steer clear of Trace
for protecting your code against contract violations. I wouldn’t want to be the developer that caused a sysop or devop to have to respond to a call in the middle of the night to close a dialog so the server/service can continue …
And even if there were some – as yet unknown to me – feature in .Net that would suppress the dialogs in code running as a Windows’ service, I still can’t use Trace
to protect my code against contract violations.
And that was surprise number four.
Surprise 4. Asserts in C# do not throw exceptions
Having read the description in the docs for the Assert
method of both the Debug
and Trace
classes, it took quite some time for the full implications of those dialogs to hit me. If that felt like lightning, the realization that followed felt like an earthquake.
Asserts in C# do not throw exceptions.
Yes, I know that when you turn off assertions for release builds in Delphi, you won’t get any exceptions either and your code is not protected from contract violations, but… they do do so in debug builds and they do do so for assertions that you “keep” in release builds even when turning off the large majority of your asserts.
So Trace.Assert
not throwing an exception when the “condition is false” is a big deal. Because when the code continues to execute in a situation with arguments that it wasn’t designed to handle, that tends to produce the most horrific and hard to debug subtle errors.
Surprise recovery
Is there a way to recover from these surprise?
Yup! Of course, there is.
To get the behavior I expect from an Assert call, I just coded up two simple classes: DebugEx
and TraceEx
. They give me the best of both worlds. The ease of selecting whether the Assert makes it into a release build from C# with the exception throwing protection of code from Delphi.
class AssertionFailure : Exception { public AssertionFailure() : base() { } public AssertionFailure(string message) : base(message) { } //public AssertionFailure(SerializationInfo info, StreamingContext context) : base(info, context) { } public AssertionFailure(string message, Exception innerException) : base(message, innerException) { } } class DebugEx { [Conditional("DEBUG")] public static void Assert(bool condition) { if (!condition) throw new AssertionFailure("Assertion condition failed."); } [Conditional("DEBUG")] public static void Assert(Boolean condition, string message) { if (!condition) throw new AssertionFailure(message); } [Conditional("DEBUG")] public static void Assert(bool condition, string message, string detailMessage) { if (!condition) throw new AssertionFailure(string.Join("\n", message, detailMessage)); } [Conditional("DEBUG")] public static void Assert(bool condition, string message, string detailMessageFormat, params object[] args) { if (!condition) throw new AssertionFailure(string.Join("\n", message, string.Format(detailMessageFormat, args))); } } class TraceEx { public static void Assert(bool condition) { if (!condition) throw new AssertionFailure("Assertion condition failed."); } public static void Assert(Boolean condition, string message) { if (!condition) throw new AssertionFailure(message); } public static void Assert(bool condition, string message, string detailMessage) { if (!condition) throw new AssertionFailure(string.Join("\n", message, detailMessage)); } public static void Assert(bool condition, string message, string detailMessageFormat, params object[] args) { if (!condition) throw new AssertionFailure(string.Join("\n", message, string.Format(detailMessageFormat, args))); } }
That’s it. Enjoy!
What had you scratching your head when moving to C#? I’d love hearing from you. Just let me know in the comments below or by email.
Apparently Debug.Assert the calls are automagically removed from assemblies compiled with the DEBUG conditional defined.
You’ve got the logic switched around. The calls are removed if the DEBUG conditional is not defined.
@David: you are absolutely right. Edited. Thanks for the catch!