Proposal: Null-conditional throw? #1140
Replies: 5 comments
-
Related discussions:
|
Beta Was this translation helpful? Give feedback.
-
If I can attack your core justification for a moment, regarding call stack preservation: One alternative to passing up an exception and rethrowing, if you follow @benaadams' adventure, is to inline the call stack to the part that throws. For example:
In Release configuration, this produces the following call stack:
Another alternative is to use |
Beta Was this translation helpful? Give feedback.
-
I worry this would bake in deoptimizations. Scoping-wise, anything to the rhs of throw is cold code; to the lhs is hot code. Representing throw
{
ExpensiveCreateException(); // new exception, message resource lookup etc
} This changes it to Exception ex = ExpensiveCreateException(); // new exception, message resource lookup etc
if (ex != null)
{
throw
{
ex;
}
} The throw itself and its scope is still moved to cold code, but the code to do with creating the exception; doing resource and string lookup is now inline and hot code? /cc @AndyAyersMS |
Beta Was this translation helpful? Give feedback.
-
If instead of E t = e;
if ((object)t != null)
throw t; The compiler could rewrite static class Bar
{
public static void Foo(string s, int i)
{
throw? Contract.NotNull(s);
throw? Contract.Range(i, 0, s.Length);
// ...
}
}
static class Contract
{
public static ArgumentNullException NotNull<T>(T argument, [CallerArgumentExpression("argument")]string paramName = null)
{
return argument == null ? new ArgumentNullException(paramName) : null;
}
public static ArgumentOutOfRangeException Range(int value, int minInclusive, int maxExclusive, [CallerArgumentExpression("value")]string paramName = null)
{
if (value < minInclusive)
return new ArgumentOutOfRangeException(paramName, $"The value should be greater than or equal to {minInclusive}.");
if (value >= maxExclusive)
return new ArgumentOutOfRangeException(paramName, $"The value should be less than {maxExclusive}.");
return null;
}
} to push the construction the right of the throw: static class Bar
{
public static void Foo(string s, int i)
{
if (Contract.NotNull(s))
{
throw Contract.GetNotNullException(s);
}
if (Contract.Range(i, 0, s.Length))
{
throw Contract.GetRangeException(i, 0, s.Length);
}
// ...
}
}
static class Contract
{
public static bool NotNull<T>(T argument)
{
return argument == null ? true : false;
}
public static bool Range(int value, int minInclusive, int maxExclusive)
{
if (value < minInclusive)
return true;
if (value >= maxExclusive)
return true;
return false;
}
public static ArgumentNullException GetNotNullException<T>(T argument, [CallerArgumentExpression("argument")]string paramName = null)
{
return new ArgumentNullException(paramName);
}
public static ArgumentOutOfRangeException GetRangeException(int value, int minInclusive, int maxExclusive, [CallerArgumentExpression("value")]string paramName = null)
{
if (value < minInclusive)
return new ArgumentOutOfRangeException(paramName, $"The value should be greater than or equal to {minInclusive}.");
return new ArgumentOutOfRangeException(paramName, $"The value should be less than {maxExclusive}.");
}
} |
Beta Was this translation helpful? Give feedback.
-
Non-Returning methods are better than direct throws; other than the extra entry in call stack; as driect throws can push out the chance to inline (itself an überoptimization; which can lead to a cascade of other optimizations) Slides 19-22 What's new for performance in .NET Core 2.0 Would be nice if the C# compiler could make use of this in some way; as the Jit doesn't split methods (non-crossgen/ngen/AoT) /cc @mikedn |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Null-conditional
throw?
Null-conditional
throw?
could be used to express contracts or argument validation logic in a concise manner.Proposal
The
throw? e
statement is equivalent towhere
t
is a temporary variable to hold the result of evaluating the expressione
, andE
is the static type ofe
, which is subject to the same typing constraints as for the existingthrow
statement.Example
An example of using
throw?
is shown below to check the inputs to a method:This example leverages the proposal on
CallerArgumentExpression
to automatically supply theparamName
argument.Note that the
Contract
class is not part of this proposal and is solely included to show an example of such methods. Alternatives are possible, including static factory methods on existing exception types:Benefits
Existing ways of expressing the example above suffer from a number of issues.
Conciseness
Combined with localization requirements, the code only gets bigger. To reduce the noise, people sometimes resort to more vague messages by combining predicates:
The
throw
expression support in C# 7.0 can help to reduce noise in select cases by combiningthrow
with the??
operator, e.g. for null checking while performing field initialization in constructors. This is a very narrow case.Call stack preservation
To reduce the noise shown in the previous section, developers sometimes resort to introducing helper methods akin to our
Contract
class. However, they also make these methods throw:While this is more concise than
throw?
, it suffers from changing the exception stack trace because the exception is thrown from the helper methods:This causes complexity when analyzing crashes, e.g. the use of immunitized stack frames in exception bucketization and crash analysis tools such as Watson.
The alternative is to return
Exception
objects from these helper methods in order to retain the original throw site. An example of this can be found in the expression tree API, e.g. inIndexExpression
:The first method,
RequiresCanRead
suffers from changing the throw site of the exception. The second method,ArgumentMustBeArray
is simply a constructor for an exception (hiding message localization logic), but fails to incorporate the check, which gets repeated in different places, making it hard to tweak and reuse (shy of introducing abool
-returning method to check for this condition).One way to improve this code could be to use an
out
parameter and optionally use C# 7.0out
variables at the use site:This way, the throw site is preserved and the helper methods incorporate the checks required. However, a lot of ceremony using a "Try" pattern with an
out
parameter is brought in.A conditional
throw?
statement enables the creation of helper methods that perform checks and optionally construct exception instances, while retaining the original throw site in the call site. In the example shown above, the result would look like:Besides conciseness and exception stack cleanliness, it also documents the possible control flow paths more clearly (e.g. it may not be obvious that
RequiresCanRead
may throw).Performance
It is assumed that the performance impact of calling helper methods to perform validation and construct an exception instance is negligible and that these small
Contract
methods are eligible (or could be explicitly marked) for inlining.Drawbacks
One drawback is the potential common pitfall of developers forgetting to use
throw?
when callingContract
-like methods, causing the returnedException
object to be discarded and never get thrown as intented.This is more of a library issue which is especially relevant if users were accustomed to existing contract frameworks where such methods typically throw.
Note that this is a more general issue with the result of method calls (or object instantiation) getting discarded:
The last example is similar to the one we're dealing with here in the proposed
throw?
statement when used withContract
methods. To deal with such issues, an unrelated feature could be proposed to annotate a return type as not discard:A warning would be generated if the result is not used, indicating a common mistake according to the API author. This is somewhat similar to the warning that results when lacking an
await
on an awaitable expression in the context of anasync
method, also indicating a common mistake. In all such cases, the user can still suppress the warning using the_
discard syntax or using a#pragma
:Given that first-class language support for discard was introduced using the
_
syntax, the friction caused by false positive warnings is far less than it was before, requiring the introduction of made-up variable names or the use of#pragma
directives.Other methods that could benefit from such an annotation are
Task.From*
methods, many methods onstring
that return new string instances, methods on immutable types (e.g.Update
on expression trees, collection editing methods on immutable collections), etc. all of which have no useful side-effect when being called other than for using the object that's returned.Note that such a hypothetical feature could be implemented using an analyzer that either has a list of known members whose result should not be discarded, or uses a return attribute convention to detect such members. Such an analyzer could also be shipped in the NuGet packages with libraries that could benefit from such use site checks (though one would benefit from the analyzer logic being shared across such libraries, given that it only needs a list of members).
Being implementable using an analyzer, it doesn't require a language change unless a no discard modifier on members would be desirable in lieu of
[return: NoDiscard]
or another means to list a member as having a no-discard return value. Note that output parameters and possibly even tuple components could benefit from no-discard annotations. A no-discard annotation could also be applied at the type level, e.g. forIDisposable
types, where one would at least expect the receiver of an instance to make an attempt at callingDispose
rather than dropping the instance.Open questions
A conditional
throw?
expressionIs there a null-conditional
throw?
expression variant? It seems to have limited use, e.g. in the context of expression-bodied members:When used in a null coalescing operator, a
throw?
expression's result could either be to throw or to return adefault
value:When used in a conditional operator, a
throw?
expression's result could either be to throw or to return adefault
value. However, does it cause lifting to null?Nullable reference types
How does the feature interact with nullable reference types proposed for C# 8.0? In particular, does a
throw?
statement degenerate to athrow
statement if the expression is known to be non-null?This question even applies without considering nullable reference types, when the expression of a
throw?
statement is known to be non-null.Conditional compilation
Is the following legal, and does it cause erasure of the entire
throw?
statement?Note the declaration of
Foo
is not legal today, failing with the following error:This would be useful to deal with e.g.
DEBUG
-only exceptions (as discussed in the next section to express invariants), but it's not clear that it warrants any revisit of theConditionalAttribute
behavior (orpartial
methods which behave similarly), for example to return thedefault
value of the return type in case the member is omitted (which would trigger a warning in C# 8.0 for non-nullable reference return types).Contract APIs
Does this language feature warrant the introduction of common contract methods in the BCL? If so, what style would be preferred?
Contract
class has the benefit of being usable withusing static
for further conciseness. However, it may cause coupling across BCL assembly boundaries.Where does this feature get positioned in relation to
System.Diagnostics.Contracts
which has limited use today, given the lack of analysis and rewriting tools?Is the feature's scope for typical pre-condition checks sufficiently large to warrant its introduction or should another attempt be made at expressing contracts (pre- and post-conditions, invariants, etc.) in lieu of this feature?
Note that
throw?
statements are also usable for expressing invariants, possibly combined with local functions, as shown below.More sophistication could be used in the sample above to make the invariant check use
Debug.Assert
inDEBUG
builds using a helper method:This allows for a more concise expression of the loop with its invariants but has the drawback of always allocating error strings. The use of a similar
Fail
method prevents this:With this, we can write:
In
DEBUG
builds, the loop would continue after an assert failure (becausenull
is returned fromFail
andLoopInvariant
), while inRELEASE
builds, a runtime exception would occur on the first line of the loop body. If the invariant is only to be checked at development time, it would ideally be conditionally compiled (as discussed in the previous section):An alternative would be to make it always return
null
inDEBUG
builds and have some optimization that can effectively eliminate all the code related to thethrow?
statement (the local and the branch). There may be some synergy here with proposed C# 8.0 work, though it'd have to track "will always be null" rather than "may be null, or, will never be null" in an inter-procedural manner.Note that these examples of invariants merely represent a stretch goal for the proposed feature. With local functions returning
void
, performing checks and unconditionally throwing, and with aConditional
attribute applied, one can already achieve expressing invariants that only run inDEBUG
builds (with the only drawback of moving the throw site to the helper method, which for a local function will have a mangled name).Specification
In this section, the proposed edits to the C# language specification are shown.
Statements
Jump statements
Jump statements unconditionally transfer control.
The
throw?
statementThe
throw?
statement null-conditionally throws an exception.A
throw?
statement with an expression throws the value produced by evaluating the expression if the evaluation of the expression does not producenull
. The expression must denote a value of the class typeSystem.Exception
, of a class type that derives fromSystem.Exception
or of a type parameter type that hasSystem.Exception
(or a subclass thereof) as its effective base class. If evaluation of the expression producesnull
, control is transferred to the end point of thethrow?
statement.Let
E
be the type of expression. Athrow?
statement of the form:is then expanded to:
For additional information on exception propagation behavior, refer to the section describing the
throw
statement.Exceptions
Causes of exceptions
Exceptions can be thrown in three different ways.
throw
statement throws an exception immediately and unconditionally. Control never reaches the statement immediately following thethrow
.throw?
statement throws an exception immediately, if the expression denoting the exception evaluates to a non-null
value.System.DivideByZeroException
if the denominator is zero.Beta Was this translation helpful? Give feedback.
All reactions