-
Notifications
You must be signed in to change notification settings - Fork 4.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
API Proposal: Debug.Assert overloads with interpolated string handler #53211
Comments
I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label. |
Tagging subscribers to this area: @tommcdon, @krwq Issue DetailsBased on #52894 (comment)... Background and MotivationDebug.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 APInamespace 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:
Usage ExamplesCode 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
|
It's appealing that this change would magically speed up existing code on recompile. But as you say (and we mentioned in the original issue) it will be visible if generating the format string causes side effects. That would be weird and likely unintentional, but nevertheless might be surprising and would require code changes to avoid. would anyone write @marklio thoughts? |
Debug.Assert is already [Conditional("DEBUG")]. From my perspective, if you're mutating state in your message, you deserve to be broken :) |
As you know, that's not necessarily justification for a breaking change, even for a SxS product 😄 I've certainly reported/fixed bugs where asserts where unintentionally mutating state, it would be ironic to create bugs where they stopped mutating state. (Maybe that's going to fix bugs..) |
I like the application to both |
That's fair but it seems somewhat fringe for customers to ship code with the |
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);
}
[Conditional("DEBUG")]
public static void WriteIf(
bool condition,
[InterpolatedStringHandlerArgument("condition")] WriteIfInterpolatedStringHandler message);
[Conditional("DEBUG")]
public static void WriteIf(
bool condition,
[InterpolatedStringHandlerArgument("condition")] WriteIfInterpolatedStringHandler message,
string category);
[Conditional("DEBUG")]
public static void WriteLineIf(
bool condition,
[InterpolatedStringHandlerArgument("condition")] WriteIfInterpolatedStringHandler message);
[Conditional("DEBUG")]
public static void WriteLineIf(
bool condition,
[InterpolatedStringHandlerArgument("condition")] WriteIfInterpolatedStringHandler message,
string category);
[InterpolatedStringHandlerAttribute]
[EditorBrowsable(EditorBrowsableState.Never)]
public struct WriteIfInterpolatedStringHandler
{
public WriteIfInterpolatedStringHandler(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);
}
}
public static class Trace
{
[Conditional("TRACE")]
public static void Assert(
bool condition,
[InterpolatedStringHandlerArgument("condition")] AssertInterpolatedStringHandler message);
[Conditional("TRACE")]
public static void Assert(
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);
}
[Conditional("TRACE")]
public static void WriteIf(
bool condition,
[InterpolatedStringHandlerArgument("condition")] WriteIfInterpolatedStringHandler message);
[Conditional("TRACE")]
public static void WriteIf(
bool condition,
[InterpolatedStringHandlerArgument("condition")] WriteIfInterpolatedStringHandler message,
string category);
[Conditional("TRACE")]
public static void WriteLineIf(
bool condition,
[InterpolatedStringHandlerArgument("condition")] WriteIfInterpolatedStringHandler message);
[Conditional("TRACE")]
public static void WriteLineIf(
bool condition,
[InterpolatedStringHandlerArgument("condition")] WriteIfInterpolatedStringHandler message,
string category);
[InterpolatedStringHandlerAttribute]
[EditorBrowsable(EditorBrowsableState.Never)]
public struct WriteIfInterpolatedStringHandler
{
public WriteIfInterpolatedStringHandler(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);
}
}
} |
Unfortunately, bringing data to this conversation is really hard. Spotting side effects in string creation is tricky statically. However, if we think about how someone would experience this break, I think risk becomes clearer. A library or app containing such code simply rolling onto a framework with this new API would not experience a break or any change in behavior. Their callsites remain bound to the string-based overloads. They would have to recompile the sensitive code against the new APIs, which puts us in source compat territory. Code sensitive to the break (where DEBUG is turned on in all used configurations so they've never experienced the conditionalness of these APIs, and string creation via interpolation has side effects) would then be broken in all configurations (readily observable/debuggable). It's kind of the optimal situation for a breaking change that delivers clear value, and the break is experienced directly by the developer compiling code (as opposed to a 3rd party). Scoped to a narrow scenario that is fundamentally opposed to recommendations and delivers easy to understand value to most customers. I tend to agree with @stephentoub and @terrajobst that this is goodness for the platform. |
@stephentoub I see this is marked blocking. Suggesting someones planning to do it? Wondering whether we should mark up for grabs. |
I added the Debug.Assert/Write{Line}If support to #51653. I didn't yet add the Trace APIs as I'm questioning whether they're really worthwhile... I'm not aware of a lot of new code being written to use Trace.Assert/Write{Line}If, and combine that with code also using interpolation in the messages... |
The Debug overloads have been added for .NET 6. I've changed the milestone to Future for the Trace overloads; we can add them if we get meaningful feedback they'd actually be helpful. |
We've not received any feedback yet about the lack of this on Trace. I'm going to close it for now. |
Based on #52894 (comment)...
Background and Motivation
Debug.Assert has three overloads today:
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:
to instead be:
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
Additional options:
Usage Examples
Code like:
would compile down to code along the lines of:
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
cc: @333fred
The text was updated successfully, but these errors were encountered: