Skip to content
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

Proposal: await? (Null-aware await) #7171

Closed
alrz opened this issue Dec 2, 2015 · 26 comments
Closed

Proposal: await? (Null-aware await) #7171

alrz opened this issue Dec 2, 2015 · 26 comments

Comments

@alrz
Copy link
Contributor

alrz commented Dec 2, 2015

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.

@alrz
Copy link
Contributor Author

alrz commented Dec 2, 2015

I was thinking that this is already proposed, but didn't find anything.

@i3arnon
Copy link

i3arnon commented Dec 2, 2015

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.

@bbarry
Copy link

bbarry commented Dec 2, 2015

👎 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.

@alrz
Copy link
Contributor Author

alrz commented Dec 2, 2015

@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.

@alrz
Copy link
Contributor Author

alrz commented Dec 2, 2015

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.

@i3arnon
Copy link

i3arnon commented Dec 2, 2015

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 nulls 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?.

@alrz
Copy link
Contributor Author

alrz commented Dec 2, 2015

@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

@bbarry
Copy link

bbarry commented Dec 2, 2015

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 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.

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().
• A bool b is obtained by evaluating the expression (a).IsCompleted.
...
• Either immediately after (if b was true), or upon later invocation of the resumption delegate (if b was false), 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 of GetResult is void or (t)?.GetAwaiter().GetResult() ?? default(T) where T is the return type of GetResult. 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.

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().
• A bool b is obtained by evaluating the expression (a).IsCompleted != false.
...
• Either immediately after (if b was true), or upon later invocation of the resumption delegate (if b was false), 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.

@alrz
Copy link
Contributor Author

alrz commented Dec 2, 2015

@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).

@bbarry
Copy link

bbarry commented Dec 2, 2015

also Quoting @gafter (#6400 (comment)):

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.

@alrz
Copy link
Contributor Author

alrz commented Dec 2, 2015

@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.

@HermanEldering
Copy link

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)).

@pawchen
Copy link
Contributor

pawchen commented Mar 6, 2016

What would it be if await?ing a Task<TStruct> where `TStruct' is value type? Compiler error?

@alrz
Copy link
Contributor Author

alrz commented Mar 6, 2016

@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.

@pawchen
Copy link
Contributor

pawchen commented Mar 7, 2016

@alrz Ok, so my question is not applicable here actually.

@paulomorgado
Copy link

?? is the null coalescing operator

@ljw1004
Copy link
Contributor

ljw1004 commented Sep 16, 2016

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.

@MichaelPuckett2
Copy link

MichaelPuckett2 commented Sep 28, 2016

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.

@JayBazuzi
Copy link

One case where I've often wished for something like await? is when getting the content of an HTTP response:

await? httpResponseMessage.Content?.ReadAsStringAsync()

@alrz
Copy link
Contributor Author

alrz commented Sep 30, 2016

@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();

@gafter
Copy link
Member

gafter commented Mar 20, 2017

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 dotnet/csharplang#35 for a proposal under consideration.

@MkazemAkhgary
Copy link

why this proposal got rejected? its perfect use case is for null-able types.

@jnm2
Copy link
Contributor

jnm2 commented Mar 7, 2019

@MkazemAkhgary

What makes you think the proposal was rejected? Read gafter's last comment.

@paulomorgado
Copy link

@MkazemAkhgary, read the last message from @gafter.

There's an open issue on the C# language repo for this: dotnet/csharplang#35

@MkazemAkhgary
Copy link

@jnm2 @paulomorgado I see, I should've noticed more carefully, thanks a lot.

@dzmitry-lahoda
Copy link

Innocent

await x.CompletedAction?.CallAsync(provider_, id, data);

NO!!!
WEIRD ERROR MESSAGE.

ugly fix

await (x.CompletedAction?.CallAsync(provider_, id, data) ?? Task.CompletedTask);
```

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests