Skip to content

API Proposal: Debug.Assert overloads with interpolated string handler #53211

Closed

Description

Based on #52894 (comment)...

Background and Motivation

Debug.Assert has three overloads today:

[Conditional("DEBUG")]
public static void Assert([DoesNotReturnIf(false)] bool condition);
 
[Conditional("DEBUG")]
public static void Assert([DoesNotReturnIf(false)] bool condition, string? message);
 
[Conditional("DEBUG")]
public static void Assert([DoesNotReturnIf(false)] bool condition, string? message, string? detailMessage);

We're generally not concerned with the performance of asserts; after all, they're debug-only code. However, heavy use of asserts, or asserts where the developer wants to log significant details about the failure, can be non-trivially expensive and slow down debug execution to the point where it's impactful. For such cases, a developer can rewrite their assert:

Debug.Assert(condition, $"Details: {GetDetails()}");

to instead be:

if (!condition)
{
    Debug.Fail($"Details: {GetDetails()}");
}

but such a transformation is a) annoying to write when it's exactly what Debug.Assert is for, and b) can start to make the code less readable.

We can take advantage of the new language support for interpolated string handlers to keep the simple code and effectively have the compiler generate the guards in such a way that the interpolation won't happen unless the assert fails.

Proposed API

namespace System.Diagnostics
{
    public static class Debug
    {
        [Conditional("DEBUG")]
        public static void Assert([DoesNotReturnIf(false)] bool condition, [InterpolatedStringHandlerArgument("condition")] AssertInterpolatedStringHandler message);
 
        [Conditional("DEBUG")]
        public static void Assert([DoesNotReturnIf(false)] bool condition, [InterpolatedStringHandlerArgument("condition")] AssertInterpolatedStringHandler message, [InterpolatedStringHandlerArgument("condition")] AssertInterpolatedStringHandler detailedMessage);

        [InterpolatedStringHandlerAttribute]
        [EditorBrowsable(EditorBrowsableState.Never)]
        public struct AssertInterpolatedStringHandler
        {
            public AssertInterpolatedStringHandler(int literalLength, int formattedCount, bool condition, out bool assert);
            public void AppendLiteral(string value);
            public void AppendFormatted<T>(T value);
            public void AppendFormatted<T>(T value, string? format);
            public void AppendFormatted<T>(T value, int alignment);
            public void AppendFormatted<T>(T value, int alignment, string? format);
            public void AppendFormatted(System.ReadOnlySpan<char> value);
            public void AppendFormatted(System.ReadOnlySpan<char> value, int alignment = 0, string? format = null);
            public void AppendFormatted(object? value, int alignment = 0, string? format = null);
            public void AppendFormatted(string? value);
            public void AppendFormatted(string? value, int alignment = 0, string? format = null);
        }
    }
}

Additional options:

  • We could expose similar overloads for Trace.Assert (they could use Debug+AssertInterpolatedStringHandler).
  • We could expose similar overloads for Debug.Write{Line}If (again using the same handler).

Usage Examples

Code like:

Debug.Assert(computed, $"{GetId()} => {GetResult()}");

would compile down to code along the lines of:

var handler = new AssertInterpolatedStringHandler(4, 2, computed, out bool assert);
if (assert)
{
    handler.AppendFormatted(GetId());
    handler.AppendLiteral(" => ");
    handler.AppendFormatted(GetResult());
}
Debug.Assert(computed, handler);

enabling the formatting and evaluation of the arguments to be done conditionally based on "assert", which would be set equal to !condition by the ctor.

Risks

  • Existing Debug.Assert calls using string interpolated arguments would start binding to the new overloads. Arguments in the format holes would start to be executed even more conditionally than they already are.
  • Using Debug.Assert(condition, $"interpolated", "non-interpolated") would silently fall back to the existing overload, since there's no proposed overload that covers all combinations of strings and handlers.

cc: @333fred

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions