-
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]: Throw helpers should return the valid value back #70515
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. |
This is binary breaking change. |
I had no idea unused return values had an overhead like this, very interesting to know. I'm kinda surprised the compiler is not able to optimize this away. Wouldn't it be able to tell that the value is not used and thus ignore it or something along those lines? Would it potentially make sense to have a variation that returns the value then, on top of the original |
The
No. Although IL allows overload by return type, all of the high level languages disallow this. |
Yeah I got that, but wouldn't it be possible to somehow avoid actually pushing the result on the stack if we knew upfront it was not being used? Wouldn't that be a possible optimization the compiler could do? Or is that not feasible because we are talking about the same function code? Maybe it could automatically generate a "void version" of the function when this situation is detected?
I specifically avoided the word "overload" since I knew we couldn't overload just on return type. By variant I meant another method with a different name (for instance, the The problem would be that people wouldn't be aware of this detail you mentioned and end up using them interchangeably for the non-returning scenario... How important is this IL optimization aspect? Was it a key component when coming up with the helpers initially? Is the restriction set in stone in such a way that we won't ever be able to return anything from the helpers because of it? |
Literally nothing can be changed around the IL semantics. The body of callee is agnostic about caller, and IL doesn't know anything about inlining.
.NET languages especially C# won't transform public API shape. The compiler would not emit anything public that's not declared by code (except record). Additionally, there's no situation for compiler to detect.
This is possible. But I didn't find anyone interested in this from the original discussion #48573. |
Tagging subscribers to this area: @dotnet/area-system-runtime Issue DetailsBackground and motivationI've been using the newly introduced However, I'm running into the following situation a lot: public SomeClass(string value)
{
ArgumentNullException.ThrowIfNull(value);
this.value = value;
} Most often, this happens on constructors, where we want to validate the value and then assign it to a field in the class. Prior to the helper method, I used to use throw expressions for this: public SomeClass(string value)
{
this.value = value ?? throw new ArgumentNullException(nameof(value));
} I think code would be a lot simpler if we could combine both of these like this: public SomeClass(string value)
{
this.value = ArgumentNullException.ThrowIfNull(value);
} By making it so that the exception validation helpers always returned the value on exit (instead of being This is how most exception guard classes/libraries work, exactly because one of the main cases is to assign the "validated" value to some backing field after validation happens. The "validate and return" pattern guarantees I'll only ever need to use one line to initialize any value and that there will be no need to repeat the value multiple times (once on validation, and another time on assignment). This is also particularly important when dealing with base classes, where we need inline validation to happen but still pass the value forward: public abstract Base
{
public Base(string value)
{
ArgumentNullException.ThrowIfNull(value);
this.value = value;
}
}
public class Inheritor : Base
{
public Inheritor(string value)
// : base(ArgumentNullException.ThrowIfNull(value)) <- this won't work
: base(value ?? throw new ArgumentNullException(nameof(value))) // <- fallback to throw expressions :(
{
}
} API ProposalCurrent: public class ArgumentNullException : ArgumentException
{
public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression("argument")] string? paramName = null);
} New: public class ArgumentNullException : ArgumentException
{
public static T ThrowIfNull<T>([NotNull] T? argument, [CallerArgumentExpression("argument")] string? paramName = null)
public static T ThrowIfNull<T>([NotNull] T? argument, [CallerArgumentExpression("argument")] string? paramName = null)
where T : struct
} This would apply to other such helpers, like As well as other proposals such as the ones for API Usagepublic SomeClass(string reference, int? value)
{
this.reference = ArgumentNullException.ThrowIfNull(reference);
this.value = ArgumentNullException.ThrowIfNull(value);
} Alternative DesignsIf I was designing this from scratch, I'd rather have these methods named like this: ArgumentNullException.EnsureNotNull(value) As "Ensure{ConditionThatIDon'tWant}" makes more sense when a value is returned, and is also pretty common among existing guard classes and libraries. It is also fairly well known that an "Ensure" method is expected to throw when the validation "fails". RisksI think my original proposal is fairly risk-free, but renaming the methods from
|
We can just different name to highlight a new semantics: public class ArgumentNullException : ArgumentException
{
public static T EnsureNotNull<T>([NotNull] T? argument, [CallerArgumentExpression("argument")] string? paramName = null);
} @julealgon , it's not possible to overload the method only by different generic constraint. In reality, we need just one method with explicit support of nullable value type. JIT is smart enough to determine whether the particular type |
My opinion is it's not worth adding a new method with a new name just to be able to write: _value = ArgumentNullException.EnsureNotNull(arg); instead of: ArgumentNullException.ThrowIfNull(arg);
_value = arg; It's still valid to write: _value = arg ?? throw new ArgumentNullException(nameof(arg)); if that's your preference. |
@sakno , you'd be incorrect. This compiles and runs correctly: public static class Guard
{
public static T EnsureNotNull<T>([NotNull] T? argument, [CallerArgumentExpression("argument")] string? paramName = null)
{
_ = argument ?? throw new ArgumentNullException(paramName);
return argument;
}
public static T EnsureNotNull<T>([NotNull] T? argument, [CallerArgumentExpression("argument")] string? paramName = null)
where T : struct
{
_ = argument ?? throw new ArgumentNullException(paramName);
return argument.Value;
}
}
public class Something
{
private readonly int number;
private readonly string text;
public Something(int? number, string? text)
{
this.number = Guard.EnsureNotNull(number);
this.text = Guard.EnsureNotNull(text);
}
} If I remove the this.number = Guard.EnsureNotNull(number); |
I mean..... I don't disagree, but that's only because the original method was poorly designed IMHO. By limiting the usages of the method you create something that is unnecessarily not orthogonal, which provides a pretty bad experience to people using the library: suddenly, it has all these exceptional cases where you have to drop it in favor of things like throw expressions and going back to having to specify the argument name... which is the whole point of the helper 😞
I wouldn't say it is necessarily "preference": certainly using a construct where you don't need to pass the argument name is a lot more robust, so its objectively better to use it when possible. Honestly, this to me is a weak argument: for any given feature, "you can always do it the old way", but that doesn't mean you should have to. If the feature was conceived properly, it should be orthogonal: people expect it to work everywhere where the old pattern was used. I just think that it should be possible to use it in all places where the old patterns were possible, and it would be incredibly simple to make it so by just allowing the method to return the value here. Now, suddenly you just can't standardize all checks in code. Every situation needs to use a different method of argument validation. This increases confusion and makes the code brittle in places where you can't use the new helper. The lack of consistency alone could even be an argument against using My question is: is the IL optimization cited by @huoyaoyuan really worth that much that it supersedes usability/API orthogonality? From this thread, I'm assuming that's the only reason why the method doesn't return anything today (if there are other reasons please let me know). |
This is totally unrelated to a new method now. The existing method cannot be changed, and will be used for ones requires it. |
I'd agree with @stephentoub here and that this is likely not something worth adding. We've already exposed APIs named This seems like, more generally, something you might want a language feature around instead, in which case I'd recommend opening a suggestion on dotnet/csharplang |
Yeah.... I understand the limitations for sure. It is too late now to change it, basically. It is just unfortunate that such a common concern was tackled with a solution that is far from orthogonal and will create even more confusion over time around argument validation. The proposed design basically forces a mix of different approaches to be used simultaneously, instead of a unified, consistent approach. I honestly feel like this was a huge missed opportunity and a classic case of premature optimization. Now that (BTW @tannergooding I know this is fairly minor, but you might want to close this as "won't fix" instead of "completed", since github allows that differentiation now. Might give more visibility in the future for people searching through the issues). |
I think it's in the eye of the beholder. I think the approach of validating and then storing is perfectly reasonable and is no more or less unified than the proposal. |
@stephentoub note that "validating and then storing" is not possible when validating inline values to pass them to base classes, which is one of the cases I described. I don't think the lack of orthogonality is subjective here. A method that returned the value would be usable in every single argument validation scenario the same way. |
The inability to write arbitrary code in base/this delegation is an issue well beyond ThrowIfNull argument validation and deserves separate language consideration rather than contorting such an API to address the lack of such support. The API would also need to be generic to return a strongly-typed reference usable in the same manner as the original, and that brings with it its own complexity and cost, plus there are a fair number of devs and analyzers that would consider such a return value (frequently ignored) to be a smell. We'll need to agree to disagree. |
Background and motivation
I've been using the newly introduced
ArgumentNullException.ThrowIfNull
method quite a bit to simplify my code and reduce the reliance onnameof
for passing the argument name.However, I'm running into the following situation a lot:
Most often, this happens on constructors, where we want to validate the value and then assign it to a field in the class.
Prior to the helper method, I used to use throw expressions for this:
I think code would be a lot simpler if we could combine both of these like this:
By making it so that the exception validation helpers always returned the value on exit (instead of being
void
methods.This is how most exception guard classes/libraries work, exactly because one of the main cases is to assign the "validated" value to some backing field after validation happens.
The "validate and return" pattern guarantees I'll only ever need to use one line to initialize any value and that there will be no need to repeat the value multiple times (once on validation, and another time on assignment).
This is also particularly important when dealing with base classes, where we need inline validation to happen but still pass the value forward:
API Proposal
Current:
New:
This would apply to other such helpers, like
ArgumentException.ThrowIfNullOrEmpty
proposed here:As well as other proposals such as the ones for
ArgumentOutOfRangeException
here:API Usage
Alternative Designs
If I was designing this from scratch, I'd rather have these methods named like this:
As "Ensure{ConditionThatIDon'tWant}" makes more sense when a value is returned, and is also pretty common among existing guard classes and libraries.
It is also fairly well known that an "Ensure" method is expected to throw when the validation "fails".
Risks
I think my original proposal is fairly risk-free, but renaming the methods from
ThrowIfX
toEnsureNotX
(orEnsureY
whenY
is the opposite ofX
) would obviously be a nasty breaking change.The text was updated successfully, but these errors were encountered: