Skip to content

Latest commit

 

History

History
178 lines (123 loc) · 13.2 KB

File metadata and controls

178 lines (123 loc) · 13.2 KB

null-conditional await

Champion issue: #8631

Summary

Support an expression of the form await? e, which awaits e if it is non-null, otherwise it results in null.

Motivation

This feature is not intended to encourage code that returns a null task from async-shaped methods. We are not trying to make patterns like Task DoSomethingAsync() { return null; } easier to consume; returning a null task from such a method would remain discouraged.

The feature exists to deal with the case where a task-returning expression happens to be null because some earlier step in the expression was null. For example:

await GetX()?.DoSomethingAsync();

Here DoSomethingAsync itself always returns a non-null Task; the Task reference is null because GetX() returned null and the ?. short-circuited. Today this expression compiles, evaluates to null, and then throws a NullReferenceException from await. The feature allows the ? to carry through to the await:

await? GetX()?.DoSomethingAsync();

which does nothing if GetX() was null, and otherwise awaits the task. This composes naturally with the existing null-propagating and null-coalescing operators.

Detailed design

await? t builds on ?. and await

await? t is the composition of two existing features: the null_conditional_member_access ?. of §12.8.8 on the operand, and the ordinary await of §12.9.8.4 on the non-null branch. Concretely, await? t has the meaning of:

((object)t == null) ? default(X) : await t        // when t is not Nullable<V>
(!t.HasValue)       ? default(X) : await t.Value  // when t has type Nullable<V>

where X is the static type of await? t and t is evaluated only once. Formally, this is the null_conditional_member_access (t)?.GetAwaiter().GetResult() per §12.8.8 — or the null_conditional_invocation_expression per §12.8.10 when GetResult() returns void — with await substituted on the non-null branch. The Nullable<V> operand case follows from §12.8.8's existing P.Value.A rule.

Operand applicability, the runtime null-test shape, result-type classification (lift to Nullable<R>, NRT annotation, pointer-type result, the type-parameter restriction on the result, and void → §12.8.10), and the single-evaluation guarantee are inherited from §12.8.8 / §12.8.10. The only normative content unique to await? is the await-semantics substitution on the non-null branch.

Grammar

§12.9.8.1 General is updated as follows.

 await_expression
     : 'await' unary_expression
+    | 'await' '?' unary_expression
     ;

The null-conditional form of await_expression ('await' '?' unary_expression, hereafter written await? t) is subject to the same placement restrictions as the existing form. For example, it is only allowed in the body of an async function.

Applicability

A new subsection is added to §12.9.8:

12.9.8.5 Applicability of null-conditional await

await? t is well-typed iff:

  • The static type S of t is admissible as the operand P of a null_conditional_member_access P?.A per §12.8.8. The runtime null-test on t is the one §12.8.8 specifies for that P.
  • When GetResult() returns non-void, its return type is admissible as the result type of A in P?.A per §12.8.8 (so, for example, an unconstrained type parameter result is a compile-time error here, just as P?.A would be).
  • t (or t.Value, when t : Nullable<V>) satisfies the awaitable pattern of §12.9.8.2.

Awaitable expressions

§12.9.8.2 is updated as follows.

Additions in bold:

The task of an await_expression is required to be awaitable. An expression t is awaitable if one of the following holds:

  • t is of compile-time type dynamic
  • ...

For await? t, the awaitable pattern (including extension-method GetAwaiter resolution) is checked on t.Value when t has type Nullable<V>, and on t otherwise.

Classification

§12.9.8.3 is updated as follows.

Additions in bold:

The expression await t is classified the same way as the expression (t).GetAwaiter().GetResult(). Thus, if the return type of GetResult is void, the await_expression is classified as nothing. If it has a non-void return type T, the await_expression is classified as a value of type T.

The classification of await? t is that of the null_conditional_member_access (t)?.GetAwaiter().GetResult() per §12.8.8, or the null_conditional_invocation_expression of the same shape per §12.8.10 when GetResult() returns void.

Note: §12.8.8's type-parameter restriction applies to the result type, not the operand. An unconstrained, interface-constrained, or notnull-constrained type parameter is permitted as the operand t (per §12.9.8.5) but is a compile-time error as the awaiter's result type R. end note

Run-time evaluation

§12.9.8.4 is updated as follows.

Additions in bold:

At run-time, the expression await t is evaluated as follows:

  • An awaiter a is obtained by evaluating the expression (t).GetAwaiter().
  • ...

At run-time, await? t is evaluated as the null_conditional_member_access (t)?.GetAwaiter().GetResult() per §12.8.8, or the null_conditional_invocation_expression per §12.8.10 when GetResult() returns void, with await on the non-null branch. Equivalently:

  • ((object)t == null) ? default(X) : await t, when t's static type is not Nullable<V>, or
  • (!t.HasValue) ? default(X) : await t.Value, when t has type Nullable<V>,

with t evaluated only once and X the type of await? t per §12.9.8.3.

Worked examples

The following tables are non-normative. They illustrate how §12.8.8 / §12.8.10 inheritance plays out for await? t.

await? is defined in terms of the awaitable pattern (§12.9.8.2) and not in terms of any specific BCL types. The framework types Task, Task<T>, ValueTask, and ValueTask<T> appear below purely as illustrative examples of, respectively, reference-type and value-type awaitables; an arbitrary user-defined class RefAwaitable or struct StructAwaitable<T> that satisfies §12.9.8.2 behaves identically to Task or ValueTask<T> with the same GetResult() return type. The rules apply uniformly.

Table A. How the operand is null-tested and where the awaitable pattern is resolved (§12.9.8.5 + §12.9.8.2, both inheriting §12.8.8):

Static type of t Null test Awaitable pattern applied to
Reference-type awaitable (e.g. Task, Task<X>, user class RefAwaitable) (object)t == null t
Nullable<V> where V is a value-type awaitable (e.g. Nullable<ValueTask>, Nullable<ValueTask<X>>, Nullable<StructAwaitable<X>>) !t.HasValue t.Value
Type parameter S without a struct constraint (includes where S : class, where S : SomeBaseClass, where S : ISomething, where S : notnull, unconstrained, …) (object)t == null (trivially false at runtime for non-nullable value-type instantiations; the JIT is expected to elide it) t
dynamic Runtime null-test on t t
Non-nullable value-type awaitable (e.g. ValueTask, ValueTask<X>, user struct StructAwaitable<X>) compile-time error
Type parameter S known to be a non-nullable value type (e.g. where S : struct, where S : unmanaged) compile-time error

Table B. How the result type of await? t is computed from R = GetResult()'s return type (§12.9.8.3 inheriting §12.8.8 / §12.8.10):

R Classification of await? t
void nothing. Permitted only at statement position (as for null_conditional_invocation_expression, §12.8.10)
Non-nullable value type (including a type parameter where T : struct) Nullable<R>
Nullable<V> for some non-nullable value type V (e.g. GetResult() returns Nullable<int>) R unchanged (e.g. Nullable<int>)
Reference type (including a type parameter where T : class) R with nullable-reference-type annotation R?
dynamic dynamic
Type parameter not known to be a reference type or a non-nullable value type (unconstrained, interface-constrained, notnull-constrained, …) compile-time error (mirrors §12.8.8 type-parameter restriction)

The Task/ValueTask behaviors readers typically think about are all mechanical cross-products of the two tables above:

  • Tasknothing
  • Nullable<ValueTask>nothing
  • Task<int>?Nullable<int>
  • Task<Nullable<int>>?Nullable<int> (via the already-nullable row of Table B)
  • Task<string>?string?
  • Task<T> where T : structNullable<T>
  • Task<T> where T : classT? (nullable reference)
  • Task<T>, T unconstrained → compile-time error (result type is a type parameter not known to be a reference type or a non-nullable value type)
  • Nullable<ValueTask<int>>Nullable<int>

Interaction and edge cases

  • Extension GetAwaiter is supported. §12.9.8.2's awaitable-pattern check (which includes extension-method GetAwaiter resolution) runs against the receiver of (t)?.GetAwaiter() per §12.8.8 — t.Value for t : Nullable<V>, t otherwise — exactly as for ordinary await on those receivers.
  • ConfigureAwait(false) returns a struct awaitable type (e.g. ConfiguredTaskAwaitable). Consequently, await? task.ConfigureAwait(false) is a compile-time error per §12.9.8.5 (non-nullable value-type operand). The intended spelling when task itself is nullable is await? task?.ConfigureAwait(false): the inner ?. produces a Nullable<ConfiguredTaskAwaitable>, which is a valid await? operand.
  • await? t is a unary_expression, not a null_conditional_member_access. It does not continue a ?. chain from its left: to apply await? to the result of x?.GetTaskAsync() the spelling is await? x?.GetTaskAsync() (await? consumes the result of the entire ?. chain), not a continuation of that chain.
  • Statement position is permitted regardless of whether GetResult() returns void or a value: await? task; where task has type Task<int>? is a valid statement (the Nullable<int> result is discarded), exactly as x?.IntReturningMethod() is a valid statement today.

Drawbacks

As with any language feature, we must question whether the additional complexity to the language is repaid in the additional clarity offered to the body of C# programs that would benefit from the feature.

Alternatives

Although it requires some boilerplate code, uses of this operator can often be replaced by an expression something like (e == null) ? null : await e or a statement like if (e != null) await e.

Unresolved questions

  • await? t where the static type of t is dynamic: the result type is dynamic (there is no distinct dynamic? type in the language). Listed for LDM confirmation rather than as a genuinely open design question.

Design meetings