Git Product home page Git Product logo

Comments (76)

ljw1004 avatar ljw1004 commented on July 17, 2024 26

I think this proposal doesn't quit hit the key scenario, for the same reason as we made the last-minute change to "short circuit ?."

Let's review that short-circuit first. We initially had users put ?. all the way through, because we said every single ?. was evaluated from left to right. If x() returned null, then x()?.y() would return null, and so you had to put ?.z else otherwise it would throw.

  x()?.y()?.z

But this wasn't so nice... (1) it created "question mark pollution" everywhere, (2) it THREW AWAY legitimate null-checking information -- in the case where x() returns non-null, and you want to assert that y() will never return null, so you want to write x()?.y().z. The left-to-right order threw away your ability to distinguish that.

Let's now look at the proposed await? operator. And pretend for a moment that await could be written in a fluent left-to-right syntax, rather than being forced to the left...

  await? x()?.y();
  x()?.y()?.<<AWAIT>>;

The proposal as it stands requires you to write await?. This has the same problems as left-to-write conditional evaluation: it peppers your code with too many question marks, and it throws away the legitimate check you might want to make that y() never returns a null Task.

...

KEY SCENARIO. All of that discussion is theoretical. Let's get concrete. The places where I've wanted this feature (and I've wanted it a lot) have almost all been to do with ?. operators on the right hand side. I reckon this is the key use-case for this scenario:

   await? customers.FirstOrDefault()?.loadedTask;

PROPOSAL2: Let's make the same short-circuit ?. evaluation order apply to await as well. So, if the await operand involved a ?. that short-circuited the remainder of the expression, then it can also short-circuit any await operator that operates on the expression.

Written out like that, the proposal feels partly natural but partly irregular/unpredictable. And to avoid the unpredictability, that's why I end up preferring @bbarry's original suggestion:

PROPOSAL3: Let's say that await always does the null-check, on the grounds that (1) awaiting null feels naturally like a no-op, (2) using the await operator is a kind of goofy way to put in a "non-null" assertion into your code, and if you really wanted a non-null check, then there are much better ways to write it.

Note: I've spent the past two months immersed in the Flow language. I really like how it lets me write clean code with a minimum of annotations and it figures them all out for me. I guess that await? feels extra busy in the light of that experience.

from csharplang.

gafter avatar gafter commented on July 17, 2024 12

Confirming @alrz 's response,

If e is of type Task, then await? e; would do nothing if e is null, and await e if it is not null.

If e is of type Task<K> where K is a value type, then await? e would yield a value of type K?.

from csharplang.

canton7 avatar canton7 commented on July 17, 2024 11

Since nobody's mentioned it yet:

I think this is relevant for IAsyncDisposable. A common pattern is foo?.Dispose() in your type's Dispose method - it's a bit counter-intuitive that this pattern doesn't carry across to await foo?.DisposeAsync(). I suspect this will catch people out.

from csharplang.

gafter avatar gafter commented on July 17, 2024 8

@ljw1004 I think there is a compatibility issue with your proposal.

If we add the feature that await automatically does the null check (whether or not that depends on the syntax of the operand), then people will write code depending on that. But that code will also compile cleanly with an earlier version of the compiler, where it will produce code that throws an exception.

I think that is a fatal flaw.

from csharplang.

CyrusNajmabadi avatar CyrusNajmabadi commented on July 17, 2024 6

But with ?. the temptation is to write await foo?.bar() to skip the null check. This is what I (for one) want to be able to do.

Sure. But i don't. And i'd much rather just say: write await? foo?.bar() if your preference is to be null-resilient for tasks.

Regarding the value type question, I assume if we used await? with a value type, the resulting type would be nullable right?

Correct.

from csharplang.

gafter avatar gafter commented on July 17, 2024 5

@ljw1004 But isn't your proposal an incompatible change? What if someone has come to depend on the code throwing a NullReferenceException? </joking>

from csharplang.

svick avatar svick commented on July 17, 2024 4

@alrz

However, it should not always check for null, only when it's being used with null-conditional member access.

I would find it confusing if await a?.b; gave different result than var task = a?.b; await task;. Something similarly confusing already happens with ?. followed by ., but in that case there is a good reason for it. What is the reason here?

from csharplang.

jnm2 avatar jnm2 commented on July 17, 2024 4

@alrz Always checking for null is no worse than what every using statement does today. It seems like the most natural thing in the world to me.

from csharplang.

CyrusNajmabadi avatar CyrusNajmabadi commented on July 17, 2024 4

I'll champion this.

from csharplang.

Rekkonnect avatar Rekkonnect commented on July 17, 2024 4

This is true but it's also worth pointing out that foreach and (I believe?) await predate ?..

Before ?., if you said await foo.bar(), you had to check foo first or were rolling the dice. So there was no legitimate reason to ever be awaiting a null value.

But with ?. the temptation is to write await foo?.bar() to skip the null check. This is what I (for one) want to be able to do.

That is correct, and as the language has been evolving towards that direction, I believe it's safe to say, such proposals like await? and foreach? have supporting grounds.

Regarding the value type question, I assume if we used await? with a value type, the resulting type would be nullable right?

In other words assume myClass.foo() returns a Task<int>. If I said: var result = await? myClass?.foo() result would be an int? right?

Yeah, that would be the most sensible design. You still have the GetValueOrDefault method anyway, if you would like the approach of defaulting to default(TStruct) instead of (TStruct?)null.

from csharplang.

legistek avatar legistek commented on July 17, 2024 4

Yeah ok. Well like I said I can live with await? as I hadn't considered the nullable value type issue, and it sounds like await? solves that then.

I still would let foreach blow past the null, but I guess I just like to live dangerously.

Ok but what's the logic for allowing using (null) then?

Legacy. Doing it again, i would absolutely not do that.

Interesting! A true believer then. :)

from csharplang.

alrz avatar alrz commented on July 17, 2024 3

I think I gave the wrong impression. I was just pointing out an issue that would arise if we want to deliberately avoid await? syntax. I think it's not "extra busy" as @ljw1004 claims. And I don't think the compiler should "always" emit null check when we're using await to make awaiting a null a no op.

from csharplang.

svick avatar svick commented on July 17, 2024 3

@bbarry @jnm2 I'm not sure this is a problem that can be solved with technical solutions. I think it's an undesirable situation when you find some code e.g. on Stack Overflow, you copy it to your project and it's broken in a subtle way, because you're using a different version of the compiler (compiler error is fine, exception isn't). And neither of your solutions changes that.

from csharplang.

gafter avatar gafter commented on July 17, 2024 3

@HaloFour No, the suggested syntax is not already legal C#. @paulhickman-a365 's idea is workable.

See https://sharplab.io/#v2:D4AQTAjAsAUCAMACEECsBuWsQGZlkQGFEBvWRC5PAFUQFkAKASkQF4A+RAOwFcAbPphiVE5SrmQAOZADZ6YZmIplhIygH029ZgDoQATiEiAvrFNw84RNVgqREgJZcALsn1tOEIeaA===

from csharplang.

Jopie64 avatar Jopie64 commented on July 17, 2024 3

@theunrepentantgeek
.await() as an extension method would be different behavior from the current await keyword, because the extension method would be a blocking call (it would block the thread). I think there is no way you currently can replicate the behavior of async/await with an extension method.

from csharplang.

huoyaoyuan avatar huoyaoyuan commented on July 17, 2024 3

I'm starting an implementation here: https://github.com/huoyaoyuan/roslyn/tree/features/conditional-await

from csharplang.

jnm2 avatar jnm2 commented on July 17, 2024 3

The LDM notes linked in the template above say:

await? is an odd kind of keyword, but changing await to not throw on null may also be an issue. Thought needed.

I'm wondering if changing await could be reconsidered. Four and a half years ago, @gafter said (#35 (comment)):

If we add the feature that await automatically does the null check (whether or not that depends on the syntax of the operand), then people will write code depending on that. But that code will also compile cleanly with an earlier version of the compiler, where it will produce code that throws an exception.

I think that is a fatal flaw.

But now we're in a world where there is a bit more appetite for breaking changes because language version changes are more opt-in than they were before, or rationale somewhat like this, if I heard correctly. This thinking could be consistently applied in the other direction. We could consider the scenario of moving to an older compiler/language version to also be more "opt-in" than it was in the past.

There's prior art in how using has always behaved. There's also not much advantage in having the option to do an await which throws NRE when the task is null (by having two forms, await and await?) now that nullable reference types have made this checkable at compile time.

from csharplang.

CyrusNajmabadi avatar CyrusNajmabadi commented on July 17, 2024 3

I agree with those suggesting to simply change await to not throw on null, just as with using.

The only code that could be negatively impacted, it seems to me, would be code that relied on the throwing of a null ref exception instead of actually checking for null.

I disagree with this, for the same reason I think that . should continue to throw with a null receiver.

Yes, it will almost always throw when it encounters a bug in the code. But that's a virtue. It tells me when I've done something very wrong. Nulls should not be papered over. They should either be explicitly expected, or they should fail catastrophically. Silently ignoring them is just a way to lead to even worse bugs happening

from csharplang.

alrz avatar alrz commented on July 17, 2024 2

Since return null would still create a Task<T> in an async method I also think making await a no op in case the expression is null is ok. However, it should not always check for null, only when it's being used with null-conditional member access. Though that might not be always the case.

from csharplang.

Joe4evr avatar Joe4evr commented on July 17, 2024 2

I'm not 100% sure await? x will provide much benefit over awaiting a completed task.

It's more for cases like var x = await foo?.SomeTask(); If foo turns out to be null, then the await still attempts to call ((Task<T>)null).GetAwaiter().GetResult() which turns into a cryptic NRE.

from csharplang.

CyrusNajmabadi avatar CyrusNajmabadi commented on July 17, 2024 2

Can I pay you $200 to do it, I can pay you before you finish

I appreciate the gesture. But i'd rather the money go to better causes :)

from csharplang.

legistek avatar legistek commented on July 17, 2024 2

Nulls should not be papered over.

Ok but what's the logic for allowing using (null) then?

await was initially designed following a condition that was taken when designing foreach; ensure that the thing is not null, and throw if it is. Such a design has great advantages, and is the healthiest possible. You cannot simply assume that in any case, the awaited task is null, requiring that you handle the result being null too.

This is true but it's also worth pointing out that foreach and (I believe?) await predate ?..

Before ?., if you said await foo.bar(), you had to check foo first or were rolling the dice. So there was no legitimate reason to ever be awaiting a null value.

But with ?. the temptation is to write await foo?.bar() to skip the null check. This is what I (for one) want to be able to do.

Regarding the value type question, I assume if we used await? with a value type, the resulting type would be nullable right?

In other words assume myClass.foo() returns a Task<int>. If I said:
var result = await? myClass?.foo()
result would be an int? right?

from csharplang.

CyrusNajmabadi avatar CyrusNajmabadi commented on July 17, 2024 2

Ok but what's the logic for allowing using (null) then?

Legacy. Doing it again, i would absolutely not do that.

from csharplang.

jamesqo avatar jamesqo commented on July 17, 2024 1

I wonder if it would make much of a performance difference to return null instead of Task.CompletedTask for tasks that complete synchronously. I'm working on an app right now that awaits a huge (thousands) amount of tasks, most of which are completed. I could check !IsCompleted on each task before awaiting it, but it would feel cleaner to do await? task.

from csharplang.

gundermanc avatar gundermanc commented on July 17, 2024 1

I had opened an issue in the Roslyn repo to add a warning for this before seeing this thread.

I don't think that the referenced await? operator addresses the primary risk of the await operator's current behavior. The lack of a null check for the await operator makes async calls using ?. a bit of a special case that the developer has to be cognizant of, and the NRE is difficult to diagnose if you haven't seen it before.

Though a breaking behavioral change is probably not possible, having a warning and changing the exception message for this case would at least aid the developer in prevention/diagnosis.

from csharplang.

paulhickman-a365 avatar paulhickman-a365 commented on July 17, 2024 1

I hadn't realised it was already illegal - I thought I was clobbering existing property names.

Thinking about it some more, I think .await() may be preferable to .await. Although it introduces additional punctuation, the act of awaiting is more like invoking a method than accessing a property so I think it is easier to understand what is happening particularly for new developers.

from csharplang.

333fred avatar 333fred commented on July 17, 2024 1

How's progress on the feature doing?

The feature is in the any time bucket and tagged needs implementation. The team is not going to do anything on it on our own, but we will accept a community contribution.

from csharplang.

jnm2 avatar jnm2 commented on July 17, 2024 1

Possibly one benefit to requiring explicit await? is that there will be a tiny pressure against it becoming more common for task-returning methods to return null, which could happen if everyone's await (at some point in the future) made this invisible to the caller. Making the caller have to do await? feels "extra"; this might be good pressure on the implementer of the method.

If the foreach? discussion goes somewhere, the same pressure might be useful to think about.

from csharplang.

jnm2 avatar jnm2 commented on July 17, 2024 1

I just thought of one backwards compatibility thing. I can't see a way around needing both await and await? forms now:

// It's better for this to throw than to evaluate as `default(int)`
// (and requiring `int? x` here seems obviously problematic: 1. breaks existing code, 2. is a pain to have to do `.Value`,
// and 3. this either penalizes every single await with a value-typed result or else the `int` vs `int?` type of the
// expression fluctuates based on the NRT annotation on the task expression 😬)
int x = await taskOfInt;

// Then this is the only way to await a maybe-null task with a value-typed result
int? x = await? taskOfInt;

from csharplang.

333fred avatar 333fred commented on July 17, 2024 1

@taori I would suggest coming and talking to us on discord, in one of the communities listed here: https://github.com/dotnet/csharplang/blob/main/Communities.md. This issue isn't likely to be a good place for the type of back and forth real time discussion you'd probably want.

from csharplang.

dmitriyse avatar dmitriyse commented on July 17, 2024

As I am understand it's port from dotnet/roslyn#7171

from csharplang.

yaakov-h avatar yaakov-h commented on July 17, 2024

awaits e if it is non-null, otherwise it results in null

What if I have Task<T> where T is a struct/scalar, or Task without a result type?

from csharplang.

alrz avatar alrz commented on July 17, 2024

awaiting Task yields void so you can't assign it. for a value type I believe it'll be T?.

from csharplang.

bbarry avatar bbarry commented on July 17, 2024

What if said change was tied to a CLR version upgrade (say for default interface methods). A compiler targeting the new CLR could let await null be a no-op. and an older compiler wouldn't be able to target the new framework.

from csharplang.

jnm2 avatar jnm2 commented on July 17, 2024

@gafter Just brainstorming. To solve the old compiler problem, it could be useful to plan to add the capacity to opt in to new compiler behaviors in the csproj SDK which translate to csc.exe switches, in the same vein as <AllowUnsafeCode> and <CheckForOverflowUnderflow>, but which would cause old compilers to error. Here's what that might look like:

Default template, allows await null

(SDK sets <AllowAwaitNull>true</AllowAwaitNull>)

Csproj, does not override AllowAwaitNull so it stays true:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net462</TargetFramework>
  </PropertyGroup>
</Project>

Class1.cs

class Class1
{
    public async Task Foo(Bar x) => await x?.WhenX;
}

Old compiler gets passed -CompatSwitch:AllowAwaitNull and is smart enough to refuse to build at all (whether or not you await) because it doesn't recognize the AllowAwaitNull switch.

New compiler emits the null check because it recognizes the switch.

In the rare scenario where you need to compile using an older compiler:

(SDK sets <AllowAwaitNull>true</AllowAwaitNull>)

Csproj, overrides AllowAwaitNull so that it can build against older compilers:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net462</TargetFramework>
    <AllowAwaitNull>false</AllowAwaitNull>
  </PropertyGroup>
</Project>

Class1.cs

class Class1
{
    public async Task Foo(Bar x) => await x?.WhenX;
}

Old compiler does not get passed -CompatSwitch:AllowAwaitNull and acts as it always has, and an analyzer sees the await x?.y and warns that you may be awaiting null.

New compiler skips the null check because it was not given the switch, and an analyzer sees the await x?.y and warns that you may be awaiting null.

from csharplang.

ljw1004 avatar ljw1004 commented on July 17, 2024

@gafter agreed it's a fatal flaw. That's a shame.

from csharplang.

yaakov-h avatar yaakov-h commented on July 17, 2024

@jamesqo Isn't that what ValueTaskis for?

from csharplang.

jamesqo avatar jamesqo commented on July 17, 2024

@yaakov-h ValueTask<T> is only available as a counterpart to the generic Task<T> type. There is no non-generic ValueTask.

from csharplang.

jamesqo avatar jamesqo commented on July 17, 2024

To be fair though, I'm not 100% sure await? x will provide much benefit over awaiting a completed task. From what I can see GetAwaiter(), and IsCompleted and GetResult() on that awaiter reduce to just a few flag checks for a completed task. Maybe it wouldn't make so much of a perf difference to have await?.

from csharplang.

jamesqo avatar jamesqo commented on July 17, 2024

@Joe4evr Yes, I know; I wasn't criticizing the feature, I meant performance benefit. Sorry for any confusion.

from csharplang.

taori avatar taori commented on July 17, 2024

I'm curious about the state of this - as is, it would not be introduced unless someone of the community implements it. Is this correct? At least i didn't see any visible branches named feature(s)/await.

I would really love this feature to exist though.

from csharplang.

alrz avatar alrz commented on July 17, 2024

"Any Time" milestone description:

We probably wouldn't devote resources to implementing this, but we'd likely accept a PR that cleanly implements a solution.

from csharplang.

taori avatar taori commented on July 17, 2024

@alrz Yes - My question was more about the "I didn't see any visible branches"-part. I've read that description too. I'm just making sure there isn't someone working on this already.

from csharplang.

Jopie64 avatar Jopie64 commented on July 17, 2024

Hi. Just a thought that came up when reading this thread.
When the nullable-reference-types feature is turned on, await? should not be necessary anymore. await (without ?) could simply return T in case of awaiting a Task<T> and return a T? in case of awaiting a Task<T>?. The compiler can detect when used erroneously.

from csharplang.

Jopie64 avatar Jopie64 commented on July 17, 2024

Sorry, changed my mind. It should error out when using await on a Task<T>?. Otherwise you cannot express that you don't want to wait on a nullable task when it is a void task or a Task<T?>.

from csharplang.

yahorsi avatar yahorsi commented on July 17, 2024

Any progress on that? It really hurts and initial issue was created back in 2015 (((

from csharplang.

ufcpp avatar ufcpp commented on July 17, 2024

https://github.com/dotnet/csharplang/milestone/14

We probably wouldn't devote resources to implementing this, but we'd likely accept a PR that cleanly implements a solution.

from csharplang.

legistek avatar legistek commented on July 17, 2024

I've been curious about this for awhile. Count me in the category of await null should simply do nothing similar to the way using (null) does nothing when it comes time to dispose. This way we can do things like await thing?.DoSomethingAsync() without having extra lines of null-checking.

Maybe this proposal is obsolete in the age of nullable reference types though, which I admit I haven't gotten into yet.

from csharplang.

paulhickman-a365 avatar paulhickman-a365 commented on July 17, 2024

In #35 (comment) ljw1004 started off his discussion with a "consider if await was fluent" scenario. Is there a reason why this couldn't actually be implemented?

i.e. make the following valid alternatives to await in front of the expression.

thing.DoSomethingAsync().await;
thing?.DoSomethingAsync().await;
thing?.DoSomethingAsync()?.await;
var t = thing.DoSomethingAsync().await;
var t = thing?.DoSomethingAsync().await;
var t = thing?.DoSomethingAsync()?.await;

This gets around the issue of ? pollution and allows you to express that thing may be null but DoSomethingAsync() shouldn't return null if it is executed. It also means the semantics of the existing syntax of putting await in front of an expression does not change.

It's arguable whether the last example is should be valid or not, as there you do have a method with return type of Task returning null. ?.await could still raise an error whilst accepting ?. operations further to the left in the expression.

It would also have the benefit outside of null coalescing that it makes it cleaner to chain operations on the results of async calls e.g.

var count =thing.DoSomethingAsync().await.Count();

Rather than:

var count =(await thing.DoSomethingAsync()).Count();

If this is considered, I'd also like to see a shortcut for ConfigureAwait(false) by giving an argument to the await - i.e. the following are equivalent:

var count =(await thing.DoSomethingAsync().ConfigureAwait(false)).Count();
var count =thing.DoSomethingAsync().ConfigureAwait(false).await.Count();
var count =thing.DoSomethingAsync().await(false).Count();

Obviously, it would introduce a conflict with properties/methods called "await" but it would be a compile time breaking change, rather than something that causes a runtime exception / change in runtime behaviour, and await has been a keyword long enough now that I can't believe any developers actually still use it as a property/method name in code that is being actively ported into new language versions, or use it in newly written code.

from csharplang.

HaloFour avatar HaloFour commented on July 17, 2024

@paulhickman-a365

Is there a reason why this couldn't actually be implemented?

The syntax you suggested is already legal in C#, there's nothing stopping some arbitrary expression from having an await property. Sure, the language could consider it as a contextual keyword where such a property doesn't exist, but that would be confusing. And this only serves to add duplicate syntax for an existing feature which doesn't solve any problems except when it comes to stylistic preferences.

from csharplang.

theunrepentantgeek avatar theunrepentantgeek commented on July 17, 2024

Using the syntax .await() is already valid C# syntax, all you need to do is to write extension methods on Task and Task<T> (Sharplap).

The .await syntax isn't currently legal C# syntax, but would collide with extension properties (#192, #11159).

from csharplang.

canton7 avatar canton7 commented on July 17, 2024

@theunrepentantgeek I'm afraid I'm missing your point. It's not valid to call an await() extension method from inside an async method, which is the case that's being discussed here.

from csharplang.

huoyaoyuan avatar huoyaoyuan commented on July 17, 2024

@CyrusNajmabadi any update on this?

from csharplang.

CyrusNajmabadi avatar CyrusNajmabadi commented on July 17, 2024

There is no update on this.

from csharplang.

huoyaoyuan avatar huoyaoyuan commented on July 17, 2024

So what does the tag Needs Implementation and milestone Any Time means? Is it welcome for implementation?

from csharplang.

CyrusNajmabadi avatar CyrusNajmabadi commented on July 17, 2024

@333fred For info. From my understanding, it means we're not talking this for 10.0, but are not rejecting it. We could take it in some future release.

from csharplang.

333fred avatar 333fred commented on July 17, 2024

Any time generally means any time 🙂. We'd accept a community contribution that implements this feature, subject to compiler team review availability. I don't expect that we have time for this in the near term, however, as we're in full swing for bigger C# 10 features at the moment and this is not a priority.

from csharplang.

Rekkonnect avatar Rekkonnect commented on July 17, 2024

I'm starting an implementation here: https://github.com/huoyaoyuan/roslyn/tree/features/conditional-await

How's progress on the feature doing?

from csharplang.

huoyaoyuan avatar huoyaoyuan commented on July 17, 2024

How's progress on the feature doing?

It's laid aside until the team approves it and assign a reviewer.

from csharplang.

huoyaoyuan avatar huoyaoyuan commented on July 17, 2024

@333fred I had completed a basic implementation. Will you assign someone to review it during C# 11 lifetime?

from csharplang.

333fred avatar 333fred commented on July 17, 2024

No, but maybe @jaredpar would.

from csharplang.

Rekkonnect avatar Rekkonnect commented on July 17, 2024

When it comes to await, I find little to no value of NRE being thrown if the Task is null, in which case I would also expect the result to be default. While this sounds hacky, await? would behave similarly, with the result being immediately changed to the respective nullable version of the return type, if any. For void-resulting tasks, it is only beneficial to do the null check implicitly.

The foreach? topic barely bears a different weight, it's essentially a much more similar case, with the differences being that code including foreach is usually less performance-ignorant (code using await tends to care much more about the complexity, rather than the execution time itself), and also you cannot operate on void-returning instances (while void is not the unit type, in which case why would you enumerate over it?). This hints that whichever approach is chosen for await?, the same will be followed for foreach?.

from csharplang.

Xyncgas avatar Xyncgas commented on July 17, 2024

because if(x !=null) await x; is what we have to do right now it increases the letters we have to type, it would be fantastic if we can get the feature

from csharplang.

CyrusNajmabadi avatar CyrusNajmabadi commented on July 17, 2024

@Xyncgas see #35 (comment)

from csharplang.

taori avatar taori commented on July 17, 2024

@CyrusNajmabadi Are there any guidelines on how a feature like this would be implemented or a branch in your memory which adds a feature (so i can learn from file changes of the PR)? I usually only write analyzers/quickfixes, but i suppose with some example on how to add such a feature i would be capable of doing this. I've cloned dotnet/roslyn and jumped back and forth to get an idea, but i guess if there are any recommended docs to read on this, implementing this feature would be a much more feasible task.

from csharplang.

CyrusNajmabadi avatar CyrusNajmabadi commented on July 17, 2024

@333fred any thoughts?

from csharplang.

legistek avatar legistek commented on July 17, 2024

I agree with those suggesting to simply change await to not throw on null, just as with using.

The only code that could be negatively impacted, it seems to me, would be code that relied on the throwing of a null ref exception instead of actually checking for null. We shouldn't let past bad practices impede future progress, especially since, as was pointed out, there is much more appetite for breaking changes now than there was several years ago, and language versions are opt-in anyway.

FWIW, I would change foreach in the same way.

from csharplang.

jnm2 avatar jnm2 commented on July 17, 2024

@legistek Changing await to not throw on null isn't feasible when awaiting Task<int> because you'd need to get an int? out of that, and this would change the meaning of existing code such as:

var someInt = await foo.SomeTaskOfInt;
// someInt is currently `int`, not `int?`

Would every await of a value-typed result start returning Nullable<T>? Almost all the time, this would not even be helpful, let alone how confusing of a change in meaning this would be in the language.

I don't think replacing throwing with using default(int) is a good idea, either.

from csharplang.

Rekkonnect avatar Rekkonnect commented on July 17, 2024

I largely agree to this, you cannot break existing behavior that much. await was initially designed following a condition that was taken when designing foreach; ensure that the thing is not null, and throw if it is. Such a design has great advantages, and is the healthiest possible. You cannot simply assume that in any case, the awaited task is null, requiring that you handle the result being null too. In the case of foreach, not much changes if you ignore the entire iteration if the iterated collection is null.

We have already crossed that path. The foreach and await account for nullability. Silently changing foreach behavior will only impact bad practices, like relying on the thrown exception. This will, however bring a slight inconsistency to the table, with await no longer taking a similar path. The healthiest addition would be foreach? and await?, which explicitly indicate the implementation detail.

from csharplang.

legistek avatar legistek commented on July 17, 2024

@legistek Changing await to not throw on null isn't feasible when awaiting Task<int> because you'd need to get an int? out of that, and this would change the meaning of existing code such as:

var someInt = await foo.SomeTaskOfInt;
// someInt is currently `int`, not `int?`

Would every await of a value-typed result start returning Nullable<T>? Almost all the time, this would not even be helpful, let alone how confusing of a change in meaning this would be in the language.

I don't think replacing throwing with using default(int) is a good idea, either.

Good point, I hadn't thought of returning non-nullable value types.

My first response would have been to go with default, but I do see how that would be a pretty significant behavior change.

from csharplang.

CyrusNajmabadi avatar CyrusNajmabadi commented on July 17, 2024

(I believe?) await predate ?..

Sure. But it postdates our belief that null-tolerance is not a virtue, which is why alter versions of the language embraced explicit null-handling as the mechanism to use, not being null-tolerant.

from csharplang.

SebastianSchumann avatar SebastianSchumann commented on July 17, 2024

In the meantime we have .Net 8.
ConfigureAwait has been implemented with ConfigureAwaitOptions. If you specify SuppressThrowing, an ArgumentOutOfRangeException is thrown if the task is not terminated correctly and should return a value.

Wouldn't that also be an option for this await implementation?

I could imagine something like this:

class Foo
{
    public async Task DoAsync() { }

    public async Task<int> GetValueAsync() => 42;
}

// usage
Foo? foo = null;
await foo?.DoAsync();        // simply does nothing - no exception is thrown
await foo?.GetValueAsync();  // simply does nothing - no exception is thrown

var x = await foo?.GetValueAsync(); // an ArgumentOutOfRange is thrown

Same behaviour for ValueTask? and ValueTask<T>?.

from csharplang.

Rekkonnect avatar Rekkonnect commented on July 17, 2024

What you're saying is that the nullability annotation of the type should control whether the task is allowed to be null and thus suppress throwing the null exception. Keep in mind that Task is a reference type, and the annotation is part of the metadata, not within the actual type system. The awaited literal's type (Type?)null is still a null used in a place where Task (or any implementing an async iterator) is expected. During runtime you have no idea about the type being converted into Type?, this would be entirely a C# detail implemented in a very hacky way, in my opinion.

from csharplang.

SebastianSchumann avatar SebastianSchumann commented on July 17, 2024

What you're saying is that the nullability annotation of the type should control whether the task is allowed to be null and thus suppress throwing the null exception.

No. My example code should only illustrate how await might work. I am aware that the nullable annotation is only metadata and you forward a null reference.

But the compiler "knows" whether the task has a return value or not. If not no exception should be thrown. If the result value is used in any way (assignment to variable, forward to using, ...) an ArgumentOutOfRangeException should be thrown in the same way like ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing) will do.

I edited my sample code to be more clear.

from csharplang.

HaloFour avatar HaloFour commented on July 17, 2024

@SebastianSchumann

But the compiler "knows" whether the task has a return value or not.

This would silently change the behavior of existing programs and affect the return type of the expression. I don't think that would be palatable. The Task<T> could always be null, even if not annotated as an NRT.

from csharplang.

SebastianSchumann avatar SebastianSchumann commented on July 17, 2024

@HaloFour
Yes you're right, this will silently change the behaviour for existing programs. But the difference is throwing a NullReferenceException vs. an ArgumentOutOfRangeException. I can accept that argument. In that case await? might be a better choice.

Btw:
I don't understand what NRTs have anything to do with this feature. I know that a reference type could be null even if it's not annotated as an NRT. If you will change the line Foo? foo = null to Foo foo = null. This will have absolutely no effect for the usage and the rest of my sample code. The ?.-operator has nothing to do with NRTs. I'm aware of this. My sample code should only show how await should behave if a null-reference has been specified.

from csharplang.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.