Currently you can't use await
and null coalescing operator together, like
var result = await (obj as IFoo)?.FooAsync();
It would be nice if we could make await
aware of nulls, like
var result = await? (obj as IFoo)?.FooAsync();
Instead of
var task = (obj as IFoo)?.FooAsync();
var result = task != null ? await task : null;
Or
var foo = obj as IFoo;
var result = foo != null ? await foo.FooAsync() : null;
Although, this can be done with pattern-matching
var result = obj is IFoo foo ? await foo.FooAsync() : null;
But still, it's too verbose for such a simple task; for await foo.Bar?.FAsync()
this wouldn't apply though.
I was thinking that this is already proposed, but didn't find anything.
I think this is more dangerous than valuable.
Awaiting a null
throws a NullReferenceException
that is extremely hard to track down. For example, it's very easy to have a non-async
Task
-returning method:
Task<string> GetNameAsync(int id)
{
// ...
return _cache.GetAsync(id);
}
That mistakenly returns null
instead of a Task
holding a null
:
Task<string> GetNameAsync(int id)
{
if (_cache.Contains(id))
{
return null;
}
// ...
return _cache.GetAsync(id);
}
Enabling awaiting a null
with await?
will make the scenario of mistakenly awaiting a null
with the regular await
much more common.
This may be mitigated with having non-nullable reference types(#5032) as a prerequisite and restricting the usage of await
only to these types leaving await?
for nullable types.
:-1: on await?
How much of a breaking change would it be if await
were changed to implicitly perform null skipping? That is:
var result = await (obj as IFoo)?.FooAsync();
wouldn't throw a null reference. Instead it would return the default value of the result type.
@i3arnon The point is not returning null from an async method, and #5032 woudn't help it, because we are using as
and it will return a nullable anyway.
I'm thinking about a more generized solution for this; if forward pipe operator (#5445) works with await
, meaning that the following is possible
var result = arg |> await Foo.BarAsync();
or with help of #5444, (from #4714 comment)
string result = await client.GetAsync("http://microsoft.com")
|> await ::Content.ReadAsStringAsync();
Then, using a null-aware forward pipe operator, the example above could be written like this:
var result = obj as IFoo ?> await ::FooAsync();
or any other syntax.
The point is not returning null from an async method, and #5032 woudn't help it, because we are using as and it will return a nullable anyway.
@alrz of course you shouldn't return null
from a task returning method, but it's possible and it happens. And when it happens it's a nasty bug that is extremely hard to find.
This is quite rare now as a regular async
method can't return null
s so you wouldn't even try to await
a null
, but if you add await?
then it will indeed become common to do so and you can easily use await
instead of await?
Non-nullable reference types help because the compiler can enforce that await
can only be used with these types and so there's no chance of a NRE. And if you do have a nullable type you must use await?
.
@bbarry
How much of a breaking change would it be if
await
were changed to implicitly perform null skipping?
That would be confusing regardless of a breaking change. I liked @i3arnon suggestion to make await
only work with non-nullables and use await?
for nullables.
await nonNullableAwaitable;
await? nullableAwaitable; // returns null
await nullableAwaitable!; // throws
await! nullableAwaitble; // might be better
By the way, #5032 needs more support in other places too, like #6563,
foreach(var item in nullableList) {} // shouldn't work
foreach?(var item in nullableList) {} // use this, instead of
if(nullableList != null) foreach(var item in nullableList) {}
foreach(var item in nullableList!) {} // note: if you prefer an exception
foreach!(var item in nullableList) {} // might be better
Or forward pipe operator (#5445),
nullable |> FuncitonTakingNonNullable(); // wouldn't work
nullable ?> FuncitonTakingNonNullable(); // use this, instead of
if(nullable != null) FuncitonTakingNonNullable(nullable);
FuncitonTakingNonNullable(nullable!); // note: this might throw
What would be confusing about it?
Currently §7.7.6 states:
7.7.6.2 Classification of await expressions
The expression
await t
is classified the same way as the expression(t).GetAwaiter().GetResult()
. Thus, if the return type ofGetResult
isvoid
, 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_.7.7.6.3 Runtime evaluation of await expressions
At runtime, the expression
await t
is evaluated as follows:
• An awaiter _a_ is obtained by evaluating the expression(t).GetAwaiter()
.
• Abool
_b_ is obtained by evaluating the expression(a).IsCompleted
.
...
• Either immediately after (if _b_ wastrue
), or upon later invocation of the resumption delegate (if _b_ wasfalse
), the expression(a).GetResult()
is evaluated. If it returns a value, that value is the result of the _await-expression_. Otherwise the result is nothing.
The spec could be changed to avoid the null reference exception:
7.7.6.2 Classification of await expressions
The expression
await t
is classified the same way as the expression(t)?.GetAwaiter().GetResult()
if the return type ofGetResult
isvoid
or(t)?.GetAwaiter().GetResult() ?? default(T)
whereT
is the return type ofGetResult
. If the return type ofGetResult
isvoid
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_.7.7.6.3 Runtime evaluation of await expressions
At runtime, the expression
await t
is evaluated as follows:
• An awaiter _a_ is obtained by evaluating the expression(t)?.GetAwaiter()
.
• Abool
_b_ is obtained by evaluating the expression(a).IsCompleted != false
.
...
• Either immediately after (if _b_ wastrue
), or upon later invocation of the resumption delegate (if _b_ wasfalse
), the expression(a)?.GetResult() ?? default(T)
is evaluated. If it returns a value, that value is the result of the _await-expression_. Otherwise the result is nothing.
As far as I can tell, the result would be exactly the same (aside from a state machine member type change and a few IL instructions) in all cases where t
is not null, and not a runtime exception otherwise.
@bbarry The problem is with this part ?? default(T)
That's why we don't have ref var
declarations either (#6183). Quoting @gafter,
because it is too magical giving local variables initial values out of thin air.
Same would be true for await
on nulls. On the other hand, with await?
, await!
etc, we don't assume anything about user intent — what if an exception is preferred? (see my updated comment above).
also Quoting @gafter (https://github.com/dotnet/roslyn/issues/6400#issuecomment-152256592):
I think adding a language construct that implicitly throws
NullReferenceException
in new and likely scenarios is not what we would want to do.
await
currently throws NullReferenceException
implicitly in what I would argue is a reasonably likely scenario and I think consistency to the language of today is better than consistency to a language that hasn't yet been specified. A future feature may need a more detailed spec change, but I think that is still preferable than a stand alone spec change to partially implement some future feature.
@bbarry I don't know what is the difference between _implicitly_ throwing NullReferenceException
or _implicitly_ not throwing it. The point is that it shouldn't be implicit.
This might be complicated on the compiler side but it might be a nice solution if ?. could look to the right hand side of the expression, and return a completed Task if an async method is called on null. The await would then just do its normal thing.
Ie instead of returning default(Task)
it should return Task.FromResult<TResult>(default(TResult))
.
What would it be if await?
ing a Task<TStruct>
where `TStruct' is value type? Compiler error?
@diryboy I think this is related to #5032 whereas await?
only makes sense for a nullable Task
(or any other awaitable) since Task<TStruct>?
still can be null in that case, and the result will be a TStruct?
. That said, to be able to use await?
on a generic Task<T>?
it must be known at compile-time that T
is a class
or struct
because as it currently specified T?
is quite different when T
is a reference type or it is a value type.
@alrz Ok, so my question is not applicable here actually.
??
is the null coalescing operator
I like @bbarry's suggestion that await null
should be a no-op (rather than throwing). It makes me thing of Console.WriteLine(null)
which also works fine.
It might not be as pretty and I'm all for the nullable await but we need the a GetValueOrDefault() just like for nullable.
Here's an extension method that works if anyone is interested.
//Extension method
public static Task<T> GetValueOrDefault<T>(this Task<T> task, T defaultValue = default(T))
=> task == null ? defaultValue : task;
//To use
public async Task<int> SaveChangesAsync()
=> await (entities?.SaveChangesAsync()).GetValueOrDefault(100);
//or
public async Task<int> SaveChangesAsync()
=> await (entities?.SaveChangesAsync()).GetValueOrDefault();
I've tested it and this works and it's easy enough for me to stick with.
One case where I've often wished for something like await?
is when getting the content of an HTTP response:
await? httpResponseMessage.Content?.ReadAsStringAsync()
@ljw1004 @bbarry
await null should be a no-op (rather than throwing)
I think this will work out especially with explicitly nullable reference types,
object result = await obj.DoAsync();
object? result = await obj?.DoAsync();
We are now taking language feature discussion on https://github.com/dotnet/csharplang for C# specific issues, https://github.com/dotnet/vblang for VB-specific features, and https://github.com/dotnet/csharplang for features that affect both languages.
See also https://github.com/dotnet/csharplang/issues/35 for a proposal under consideration.
why this proposal got rejected? its perfect use case is for null-able types.
@MkazemAkhgary
What makes you think the proposal was rejected? Read gafter's last comment.
@MkazemAkhgary, read the last message from @gafter.
There's an open issue on the C# language repo for this: https://github.com/dotnet/csharplang/issues/35
@jnm2 @paulomorgado I see, I should've noticed more carefully, thanks a lot.
Innocent
await x.CompletedAction?.CallAsync(provider_, id, data);
NO!!!
WEIRD ERROR MESSAGE.
ugly fix
`
await (x.CompletedAction?.CallAsync(provider_, id, data) ?? Task.CompletedTask);
Most helpful comment
I like @bbarry's suggestion that
await null
should be a no-op (rather than throwing). It makes me thing ofConsole.WriteLine(null)
which also works fine.