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]: Consider changing return types of new throw helpers #86341
Comments
Tagging subscribers to this area: @dotnet/area-system-runtime Issue DetailsIn .NET 6 we introduced: class ArgumentNullException
{
+ public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null);
} In .NET 7 we introduced: class ArgumentNullException
{
+ public static void ThrowIfNull([NotNull] void* argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null);
}
class ArgumentException
{
+ public static void ThrowIfNullOrEmpty([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null);
} Now in .NET 8, we've introduced: class ArgumentException
{
+ public static void ThrowIfNullOrWhiteSpace([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null);
}
class ArgumentOutOfRangeException
{
+ public static void ThrowIfEqual<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IEquatable<T>?;
+ public static void ThrowIfGreaterThan<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IComparable<T>;
+ public static void ThrowIfGreaterThanOrEqual<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IComparable<T>;
+ public static void ThrowIfLessThan<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IComparable<T>;
+ public static void ThrowIfLessThanOrEqual<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IComparable<T>;
+ public static void ThrowIfNegative<T>(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : INumberBase<T>;
+ public static void ThrowIfNegativeOrZero<T>(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : INumberBase<T>;
+ public static void ThrowIfNotEqual<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IEquatable<T>?;
+ public static void ThrowIfZero<T>(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : INumberBase<T>;
} We can't change the 3 methods we shipped in .NET 6 and .NET 7, but we still have time to tweak the new 10 methods we're shipping in .NET 8. In particular, we've had some requests for returning the input being validated, e.g. that would make them instead be: class ArgumentException
{
+ public static string ThrowIfNullOrWhiteSpace([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null);
}
class ArgumentOutOfRangeException
{
+ public static T ThrowIfEqual<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IEquatable<T>?;
+ public static T ThrowIfGreaterThan<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IComparable<T>;
+ public static T ThrowIfGreaterThanOrEqual<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IComparable<T>;
+ public static T ThrowIfLessThan<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IComparable<T>;
+ public static T ThrowIfLessThanOrEqual<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IComparable<T>;
+ public static T ThrowIfNegative<T>(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : INumberBase<T>;
+ public static T ThrowIfNegativeOrZero<T>(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : INumberBase<T>;
+ public static T ThrowIfNotEqual<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IEquatable<T>?;
+ public static T ThrowIfZero<T>(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : INumberBase<T>;
} at which point we would also do so for all new throw helpers of the same ilk. Should we?
|
Do you have some good examples of how we would change the code in this repo to take advantage of the return value? |
I don't think we would. It ends up being a stylistic request for folks that prefer the style of doing things today like: int value = input >= 0 ? input : throw new ArgumentOutOfRangeException(...); and would like to instead do: int value = ArgumentOutOfRangeException.ThrowIfNegative(input); rather than: ArgumentOutOfRangeException.ThrowIfNegative(input);
int value = input; I'm personally not a fan of that style, but I've opened this to represent those who are :) (we discussed it briefly when adding ThrowIfNullOrWhiteSpace and said we'd have a follow-on conversation after I opened an issue for it) The one place this currently has meaningful "I couldn't otherwise use the API" impact is in places where only expressions are usable, such as when delegating to another this/base ctor: public Blah(int input) : base(ArgumentOutOfRangeException.ThrowIfNegative(input)) { ... } It's possible we'd use that somewhere in dotnet/runtime, but I can't think of any off the top of my head. I do know of some places where we might have done that for ArgumentNullException.ThrowIfNull, but only if ThrowIfNull returned the T input, and what we shipped neither returns anything nor is generic. For inlining/size reasons we made it non-generic, and once it's non-generic returning the instance as |
Maybe also worth noting (other than that assignment style Stephen already showed, which I also personally really dislike), making these APIs return a value would trigger a ton of warnings for everyone having warnings enabled for implicitly discarding return values. All those developers would have to always use |
Why not just use It is not just a style. Creating extra unused return values and extra locals has potential negative JIT throughput and code quality impact. The JIT should be able to clean it up for the simple cases, but complex methods can see some hit. |
I'm also not a huge fan of this style; but the other place it comes up is in places like |
That was just me quickly writing something out. Often the thing being assigned to is, e.g., a field: _value = ArgumentOutOfRangeException.ThrowIfNegative(input); Again, I'm not actually pushing for this, just trying to represent the viewpoint. |
Personally, I would have liked public Foo(string arg)
{
_field = ArgumentNullException.ThrowIfNull(arg);
} But if changing |
I also agree we shouldn't change this. But just curious wrt the shipped ones - would we consider changing void return type a breaking change? (To reflection I guess?) I thought we did not. |
Changing the return type of a method would break all compiled code using it; the return type of a method is part of the IL signature. |
You're right, and we document it I see.
|
Without this change, it is impossible to use those helpers in a switch expression. I already started to define my own ThrowHelpers with T as the return value instead of using the built-in void version of ThrowHelpers because of this. Foo x = obj switch
{
null => Throw<Foo>()
} But I still don't see the value to do this in the BCL. C# doesn't have bottom types ( |
Worth noting, the way throw helpers are generally used in switch expressions (eg. to throw on the default case and whatnot) is to have a branch that just returns a throw helper return. That is, in this scenario you'd use a throw helper that throws unconditionally. That is not the case for the API whose signature is being proposed for change here, as they all have conditions. So I'm not sure the example of switch expressions could really be used in this case either 🤔 |
I've seen a lot of discussion and feelings whether these should return class ArgumentNullException
{
public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null);
public static void ThrowIfNull([NotNull] void* argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null);
+ public static T EnsureNotNull<T>(T? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null);
}
class ArgumentException
{
public static void ThrowIfNullOrEmpty([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null);
+ public static void ThrowIfNullOrWhiteSpace([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null);
+ public static string EnsureNotNullOrEmpty(string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null);
+ public static string EnsureNotNullOrWhiteSpace(string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null);
}
class ArgumentOutOfRangeException
{
+ public static void ThrowIfEqual<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IEquatable<T>?;
+ public static void ThrowIfGreaterThan<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IComparable<T>;
+ public static void ThrowIfGreaterThanOrEqual<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IComparable<T>;
+ public static void ThrowIfLessThan<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IComparable<T>;
+ public static void ThrowIfLessThanOrEqual<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IComparable<T>;
+ public static void ThrowIfNegative<T>(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : INumberBase<T>;
+ public static void ThrowIfNegativeOrZero<T>(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : INumberBase<T>;
+ public static void ThrowIfNotEqual<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IEquatable<T>?;
+ public static void ThrowIfZero<T>(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : INumberBase<T>;
+ public static T EnsureNotEqual<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IEquatable<T>?;
+ public static T EnsureLessThanOrEqual<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IComparable<T>;
+ public static T EnsureLessThan<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IComparable<T>;
+ public static T EnsureGreaterThanOrEqual<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IComparable<T>;
+ public static T EnsureGreaterThan<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IComparable<T>;
+ public static T EnsurePositive<T>(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : INumberBase<T>;
+ public static T EnsurePositiveOrZero<T>(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : INumberBase<T>;
+ public static T EnsureNegative<T>(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : INumberBase<T>;
+ public static T EnsureNegativeOrZero<T>(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : INumberBase<T>;
+ public static T EnsureEqual<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IEquatable<T>?;
+ public static T EnsureNotZero<T>(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : INumberBase<T>;
+ public static T EnsureZero<T>(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : INumberBase<T>;
} |
In general this is something we try to avoid in API design, as it is more cognitive load. You hit dot, see the list, and now have to check docs to figure out which you want. |
But isn't that also the case with all those |
|
I was referring to the cognitive load mentioned by @danmoseley, not to the return value. |
The
|
namespace System;
public partial class ArgumentException
{
public static string ThrowIfNullOrWhiteSpace([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null);
}
public partial class ArgumentOutOfRangeException
{
public static T ThrowIfEqual<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IEquatable<T>?;
public static T ThrowIfGreaterThan<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IComparable<T>;
public static T ThrowIfGreaterThanOrEqual<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IComparable<T>;
public static T ThrowIfLessThan<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IComparable<T>;
public static T ThrowIfLessThanOrEqual<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IComparable<T>;
public static T ThrowIfNegative<T>(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : INumberBase<T>;
public static T ThrowIfNegativeOrZero<T>(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : INumberBase<T>;
public static T ThrowIfNotEqual<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : IEquatable<T>?;
public static T ThrowIfZero<T>(T value, [CallerArgumentExpression(nameof(value))] string? paramName = null) where T : INumberBase<T>;
} |
In .NET 6 we introduced:
class ArgumentNullException { + public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null); }
In .NET 7 we introduced:
Now in .NET 8, we've introduced:
We can't change the 3 methods we shipped in .NET 6 and .NET 7, but we still have time to tweak the new 10 methods we're shipping in .NET 8. In particular, we've had some requests for returning the input being validated, e.g. that would make them instead be:
at which point we would also do so for all new throw helpers of the same ilk.
Should we?
The text was updated successfully, but these errors were encountered: