Flussi asincroniAsync Streams
- [x] proposto[x] Proposed
- Prototipo [x][x] Prototype
- [] Implementazione[ ] Implementation
- [] Specifica[ ] Specification
RiepilogoSummary
C# include il supporto per metodi iteratori e metodi asincroni, ma non supporta un metodo che sia un iteratore che un metodo asincrono.C# has support for iterator methods and async methods, but no support for a method that is both an iterator and an async method. È necessario risolvere il problema consentendo l' await
utilizzo di in un nuovo tipo di async
iteratore, che restituisce un oggetto IAsyncEnumerable<T>
o IAsyncEnumerator<T>
anziché un oggetto o, che può essere utilizzato IEnumerable<T>
IEnumerator<T>
IAsyncEnumerable<T>
in un nuovo oggetto await foreach
.We should rectify this by allowing for await
to be used in a new form of async
iterator, one that returns an IAsyncEnumerable<T>
or IAsyncEnumerator<T>
rather than an IEnumerable<T>
or IEnumerator<T>
, with IAsyncEnumerable<T>
consumable in a new await foreach
. IAsyncDisposable
Viene inoltre utilizzata un'interfaccia per abilitare la pulizia asincrona.An IAsyncDisposable
interface is also used to enable asynchronous cleanup.
Discussione correlataRelated discussion
Progettazione dettagliataDetailed design
InterfacceInterfaces
IAsyncDisposableIAsyncDisposable
C'è stata una discussione molto IAsyncDisposable
https://github.com/dotnet/roslyn/issues/114) interessante, ad esempio, e se si tratta di una buona idea.There has been much discussion of IAsyncDisposable
(e.g. https://github.com/dotnet/roslyn/issues/114) and whether it's a good idea. Tuttavia, si tratta di un concetto necessario per aggiungere supporto per gli iteratori asincroni.However, it's a required concept to add in support of async iterators. Poiché finally
i blocchi possono contenere await
e, poiché i finally
blocchi devono essere eseguiti come parte dell'eliminazione degli iteratori, è necessaria l'eliminazione asincrona.Since finally
blocks may contain await
s, and since finally
blocks need to be run as part of disposing of iterators, we need async disposal. È anche di solito utile ogni volta che la pulizia delle risorse potrebbe richiedere un certo periodo di tempo, ad esempio la chiusura di file (che richiedono lo scaricamento), la deregistrazione dei callback e la possibilità di individuare il momento in cui la registrazione è stata completata e così via.It's also just generally useful any time cleaning up of resources might take any period of time, e.g. closing files (requiring flushes), deregistering callbacks and providing a way to know when deregistration has completed, etc.
L'interfaccia seguente viene aggiunta alle librerie .NET di base (ad esempio System. private. CoreLib/System. Runtime):The following interface is added to the core .NET libraries (e.g. System.Private.CoreLib / System.Runtime):
namespace System
{
public interface IAsyncDisposable
{
ValueTask DisposeAsync();
}
}
Come con Dispose
, richiamando DisposeAsync
più volte è accettabile e le chiamate successive dopo la prima devono essere considerate come pon, restituendo un'attività completata in modo sincrono ( DisposeAsync
non è necessario che sia thread-safe, ma non deve supportare la chiamata simultanea).As with Dispose
, invoking DisposeAsync
multiple times is acceptable, and subsequent invocations after the first should be treated as nops, returning a synchronously completed successful task (DisposeAsync
need not be thread-safe, though, and need not support concurrent invocation). Inoltre, i tipi possono implementare sia IDisposable
che e IAsyncDisposable
, in caso affermativo, è accettabile richiamarli Dispose
e viceversa DisposeAsync
, ma solo il primo dovrebbe essere significativo e le chiamate successive di devono essere di tipo NOP.Further, types may implement both IDisposable
and IAsyncDisposable
, and if they do, it's similarly acceptable to invoke Dispose
and then DisposeAsync
or vice versa, but only the first should be meaningful and subsequent invocations of either should be a nop. Di conseguenza, se un tipo implementa entrambi, è consigliabile che gli utenti chiamino una sola volta il metodo più pertinente in base al contesto, Dispose
in contesti sincroni e DisposeAsync
in quelli asincroni.As such, if a type does implement both, consumers are encouraged to call once and only once the more relevant method based on the context, Dispose
in synchronous contexts and DisposeAsync
in asynchronous ones.
(Sto lasciando una discussione su come IAsyncDisposable
interagisce con using
a una discussione separata.(I'm leaving discussion of how IAsyncDisposable
interacts with using
to a separate discussion. E la copertura del modo in cui interagisce con foreach
viene gestita più avanti in questa proposta.And coverage of how it interacts with foreach
is handled later in this proposal.)
Alternative considerate:Alternatives considered:
DisposeAsync
acceptingCancellationToken
a: in teoria, è opportuno annullare qualsiasi operazione asincrona. l'eliminazione è relativa alla pulizia, alla chiusura di elementi, alle risorse free'ing e così via, che in genere non è un elemento che deve essere annullato. la pulizia è ancora importante per il lavoro annullato.DisposeAsync
accepting aCancellationToken
: while in theory it makes sense that anything async can be canceled, disposal is about cleanup, closing things out, free'ing resources, etc., which is generally not something that should be canceled; cleanup is still important for work that's canceled. Lo stesso oggettoCancellationToken
che ha causato l'annullamento del lavoro effettivo è in genere lo stesso token passato aDisposeAsync
, rendendo inutile il fatto che l'DisposeAsync
annullamento del lavoro provocherebbeDisposeAsync
un NOP.The sameCancellationToken
that caused the actual work to be canceled would typically be the same token passed toDisposeAsync
, makingDisposeAsync
worthless because cancellation of the work would causeDisposeAsync
to be a nop. Se un utente vuole evitare di essere bloccato in attesa di eliminazione, può evitare di aspettare il risultatoValueTask
oppure attendere solo per un certo periodo di tempo.If someone wants to avoid being blocked waiting for disposal, they can avoid waiting on the resultingValueTask
, or wait on it only for some period of time.DisposeAsync
restituzioneTask
di un oggetto: ora che è presente un non generico e cheValueTask
può essere costruito da unIValueTaskSource
oggetto, la restituzioneValueTask
daDisposeAsync
consente di riutilizzare un oggetto esistente come suggerimento che rappresenta il completamento asincrono finale diDisposeAsync
, salvando un'Task
allocazione nel caso in cui vengaDisposeAsync
completata in modo asincrono.DisposeAsync
returning aTask
: Now that a non-genericValueTask
exists and can be constructed from anIValueTaskSource
, returningValueTask
fromDisposeAsync
allows an existing object to be reused as the promise representing the eventual async completion ofDisposeAsync
, saving aTask
allocation in the case whereDisposeAsync
completes asynchronously.- Configurazione
DisposeAsync
conbool continueOnCapturedContext
(ConfigureAwait
): Sebbene ci siano problemi relativi al modo in cui tale concetto viene esposto ausing
,foreach
, e altri costrutti di linguaggio che lo utilizzano, dal punto di vista dell'interfaccia, non esegue alcuna operazioneawait
e non è necessario configurare alcun elemento... Gli utenti diValueTask
possono utilizzarlo tuttavia.ConfiguringDisposeAsync
with abool continueOnCapturedContext
(ConfigureAwait
): While there may be issues related to how such a concept is exposed tousing
,foreach
, and other language constructs that consume this, from an interface perspective it's not actually doing anyawait
'ing and there's nothing to configure... consumers of theValueTask
can consume it however they wish. - eredità: poiché è necessario usare solo uno o l'altro, non ha senso forzare i tipi a implementare entrambi.
IAsyncDisposable
IDisposable
IAsyncDisposable
inheritingIDisposable
: Since only one or the other should be used, it doesn't make sense to force types to implement both. IDisposableAsync
invece diIAsyncDisposable
: è stato seguito il nome che gli elementi o i tipi sono "Async something" mentre le operazioni sono "done Async", quindi i tipi hanno "Async" come prefisso e i metodi hanno "Async" come suffisso.IDisposableAsync
instead ofIAsyncDisposable
: We've been following the naming that things/types are an "async something" whereas operations are "done async", so types have "Async" as a prefix and methods have "Async" as a suffix.
IAsyncEnumerable / IAsyncEnumeratorIAsyncEnumerable / IAsyncEnumerator
Vengono aggiunte due interfacce alle librerie .NET di base:Two interfaces are added to the core .NET libraries:
namespace System.Collections.Generic
{
public interface IAsyncEnumerable<out T>
{
IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default);
}
public interface IAsyncEnumerator<out T> : IAsyncDisposable
{
ValueTask<bool> MoveNextAsync();
T Current { get; }
}
}
Il consumo tipico (senza funzionalità aggiuntive del linguaggio) avrà un aspetto simile al seguente:Typical consumption (without additional language features) would look like:
IAsyncEnumerator<T> enumerator = enumerable.GetAsyncEnumerator();
try
{
while (await enumerator.MoveNextAsync())
{
Use(enumerator.Current);
}
}
finally { await enumerator.DisposeAsync(); }
Opzioni scartate considerate:Discarded options considered:
Task<bool> MoveNextAsync(); T current { get; }
:Task<bool>
L'utilizzo di supporta l'utilizzo di un oggetto attività memorizzato nella cache per rappresentare chiamate sincrone e con esito positivoMoveNextAsync
, ma è comunque necessaria un'allocazione per il completamento asincrono.Task<bool> MoveNextAsync(); T current { get; }
: UsingTask<bool>
would support using a cached task object to represent synchronous, successfulMoveNextAsync
calls, but an allocation would still be required for asynchronous completion. RestituendoValueTask<bool>
, viene abilitato l'oggetto enumeratore per l'implementazioneIValueTaskSource<bool>
e l'utilizzo come supporto per l'oggettoValueTask<bool>
restituito daMoveNextAsync
, che a sua volta consente un sovraccarico significativamente ridotto.By returningValueTask<bool>
, we enable the enumerator object to itself implementIValueTaskSource<bool>
and be used as the backing for theValueTask<bool>
returned fromMoveNextAsync
, which in turn allows for significantly reduced overheads.ValueTask<(bool, T)> MoveNextAsync();
: Non solo è più difficile da utilizzare, ma significa che nonT
può più essere covariante.ValueTask<(bool, T)> MoveNextAsync();
: It's not only harder to consume, but it means thatT
can no longer be covariant.ValueTask<T?> TryMoveNextAsync();
: Non covariante.ValueTask<T?> TryMoveNextAsync();
: Not covariant.Task<T?> TryMoveNextAsync();
: Non covariante, allocazioni a ogni chiamata e così via.Task<T?> TryMoveNextAsync();
: Not covariant, allocations on every call, etc.ITask<T?> TryMoveNextAsync();
: Non covariante, allocazioni a ogni chiamata e così via.ITask<T?> TryMoveNextAsync();
: Not covariant, allocations on every call, etc.ITask<(bool,T)> TryMoveNextAsync();
: Non covariante, allocazioni a ogni chiamata e così via.ITask<(bool,T)> TryMoveNextAsync();
: Not covariant, allocations on every call, etc.Task<bool> TryMoveNextAsync(out T result);
: Ilout
risultato deve essere impostato quando l'operazione viene restituita in modo sincrono, non quando l'attività viene completata in modo asincrono in futuro, a quel punto non è possibile comunicare il risultato.Task<bool> TryMoveNextAsync(out T result);
: Theout
result would need to be set when the operation returns synchronously, not when it asynchronously completes the task potentially sometime long in the future, at which point there'd be no way to communicate the result.IAsyncEnumerator<T>
non implementazioneIAsyncDisposable
: è possibile scegliere di separare questi.IAsyncEnumerator<T>
not implementingIAsyncDisposable
: We could choose to separate these. Tuttavia, in questo modo si complicano alcune altre aree della proposta, perché il codice deve essere in grado di gestire la possibilità che un enumeratore non fornisca l'eliminazione, rendendo difficile la scrittura di helper basati su modelli.However, doing so complicates certain other areas of the proposal, as code must then be able to deal with the possibility that an enumerator doesn't provide disposal, which makes it difficult to write pattern-based helpers. Inoltre, per gli enumeratori sarà necessario disporre di un'eliminazione (ad esempio, qualsiasi iteratore asincrono C# con un blocco finally, la maggior parte degli elementi che enumerano i dati da una connessione di rete e così via) e, se non è possibile, è semplice implementare il metodo esclusivamente comepublic ValueTask DisposeAsync() => default(ValueTask);
con un overhead aggiuntivo minimo.Further, it will be common for enumerators to have a need for disposal (e.g. any C# async iterator that has a finally block, most things enumerating data from a network connection, etc.), and if one doesn't, it is simple to implement the method purely aspublic ValueTask DisposeAsync() => default(ValueTask);
with minimal additional overhead.- _
IAsyncEnumerator<T> GetAsyncEnumerator()
: Nessun parametro del token di annullamento._IAsyncEnumerator<T> GetAsyncEnumerator()
: No cancellation token parameter.
Alternativa valida:Viable alternative:
namespace System.Collections.Generic
{
public interface IAsyncEnumerable<out T>
{
IAsyncEnumerator<T> GetAsyncEnumerator();
}
public interface IAsyncEnumerator<out T> : IAsyncDisposable
{
ValueTask<bool> WaitForNextAsync();
T TryGetNext(out bool success);
}
}
TryGetNext
viene usato in un ciclo interno per utilizzare elementi con una singola chiamata di interfaccia, purché siano disponibili in modo sincrono.TryGetNext
is used in an inner loop to consume items with a single interface call as long as they're available synchronously. Quando l'elemento successivo non può essere recuperato in modo sincrono, restituisce false e ogni volta che restituisce false, un chiamante deve successivamente richiamare WaitForNextAsync
per attendere che l'elemento successivo sia disponibile o per determinare che non sarà mai presente un altro elemento.When the next item can't be retrieved synchronously, it returns false, and any time it returns false, a caller must subsequently invoke WaitForNextAsync
to either wait for the next item to be available or to determine that there will never be another item. Il consumo tipico (senza funzionalità aggiuntive del linguaggio) avrà un aspetto simile al seguente:Typical consumption (without additional language features) would look like:
IAsyncEnumerator<T> enumerator = enumerable.GetAsyncEnumerator();
try
{
while (await enumerator.WaitForNextAsync())
{
while (true)
{
int item = enumerator.TryGetNext(out bool success);
if (!success) break;
Use(item);
}
}
}
finally { await enumerator.DisposeAsync(); }
Il vantaggio è costituito da due riduzioni, una secondaria e una principale:The advantage of this is two-fold, one minor and one major:
- Minor: consente a un enumeratore di supportare più consumer.Minor: Allows for an enumerator to support multiple consumers. Potrebbero esserci scenari in cui è utile che un enumeratore supporti più consumer simultanei.There may be scenarios where it's valuable for an enumerator to support multiple concurrent consumers. Non è possibile ottenere questo risultato quando
MoveNextAsync
eCurrent
sono separati in modo che un'implementazione non possa renderne l'utilizzo atomico.That can't be achieved whenMoveNextAsync
andCurrent
are separate such that an implementation can't make their usage atomic. Al contrario, questo approccio fornisce un unico metodoTryGetNext
che supporta il push dell'enumeratore in avanti e il recupero dell'elemento successivo, in modo che l'enumeratore possa abilitare l'atomicità se lo si desidera.In contrast, this approach provides a single methodTryGetNext
that supports pushing the enumerator forward and getting the next item, so the enumerator can enable atomicity if desired. Tuttavia, è probabile che questi scenari possano essere abilitati anche fornendo a ogni consumer il proprio enumeratore da un enumerabile condiviso.However, it's likely that such scenarios could also be enabled by giving each consumer its own enumerator from a shared enumerable. Inoltre, non è necessario imporre che ogni enumeratore supporti l'utilizzo simultaneo, in quanto in questo modo si aggiungono sovraccarichi non banali alla maggior parte dei casi che non lo richiedono, il che significa che un consumer dell'interfaccia non è in genere in grado di basarsi su questo metodo.Further, we don't want to enforce that every enumerator support concurrent usage, as that would add non-trivial overheads to the majority case that doesn't require it, which means a consumer of the interface generally couldn't rely on this any way. - Principale: prestazioni.Major: Performance. L'
MoveNextAsync
/Current
approccio richiede due chiamate di interfaccia per ogni operazione, mentre il caso migliore perWaitForNextAsync
/TryGetNext
è che la maggior parte delle iterazioni viene completata in modo sincrono, abilitando un ciclo interno stretto conTryGetNext
, in modo da avere una sola chiamata di interfaccia per ogni operazione.TheMoveNextAsync
/Current
approach requires two interface calls per operation, whereas the best case forWaitForNextAsync
/TryGetNext
is that most iterations complete synchronously, enabling a tight inner loop withTryGetNext
, such that we only have one interface call per operation. Questo può avere un effetto misurabile nelle situazioni in cui le chiamate di interfaccia dominano il calcolo.This can have a measurable impact in situations where the interface calls dominate the computation.
Tuttavia, esistono svantaggi non banali, tra cui una maggiore complessità quando questi vengono utilizzati manualmente e una maggiore probabilità di introdurre bug quando vengono utilizzati.However, there are non-trivial downsides, including significantly increased complexity when consuming these manually, and an increased chance of introducing bugs when using them. Mentre i vantaggi in merito alle prestazioni vengono visualizzati in microbenchmark, non ci riteniamo che saranno interessati dalla maggior parte dell'utilizzo reale.And while the performance benefits show up in microbenchmarks, we don't believe they'll be impactful in the vast majority of real usage. Se si scopre che sono, è possibile introdurre un secondo set di interfacce in modo leggero.If it turns out they are, we can introduce a second set of interfaces in a light-up fashion.
Opzioni scartate considerate:Discarded options considered:
ValueTask<bool> WaitForNextAsync(); bool TryGetNext(out T result);
: iout
parametri non possono essere covarianti.ValueTask<bool> WaitForNextAsync(); bool TryGetNext(out T result);
:out
parameters can't be covariant. È anche presente un piccolo effetto (un problema con il modello try in generale) che probabilmente comporta una barriera di scrittura di runtime per i risultati del tipo di riferimento.There's also a small impact here (an issue with the try pattern in general) that this likely incurs a runtime write barrier for reference type results.
AnnullamentoCancellation
Esistono diversi approcci possibili per supportare l'annullamento:There are several possible approaches to supporting cancellation:
IAsyncEnumerable<T>
/IAsyncEnumerator<T>
sono indipendenti dall'annullamento:CancellationToken
non viene visualizzato in un punto qualsiasi.IAsyncEnumerable<T>
/IAsyncEnumerator<T>
are cancellation-agnostic:CancellationToken
doesn't appear anywhere. L'annullamento viene ottenuto tramite la cottura logica dell'oggettoCancellationToken
in Enumerable e/o enumeratore nel modo appropriato, ad esempio quando si chiama un iteratore, passandoCancellationToken
come argomento al metodo iteratore e utilizzandolo nel corpo dell'iteratore, come avviene con qualsiasi altro parametro.Cancellation is achieved by logically baking theCancellationToken
into the enumerable and/or enumerator in whatever manner is appropriate, e.g. when calling an iterator, passing theCancellationToken
as an argument to the iterator method and using it in the body of the iterator, as is done with any other parameter.IAsyncEnumerator<T>.GetAsyncEnumerator(CancellationToken)
: Si passa aCancellationToken
GetAsyncEnumerator
eMoveNextAsync
le operazioni successive lo rispettano, ma è possibile.IAsyncEnumerator<T>.GetAsyncEnumerator(CancellationToken)
: You pass aCancellationToken
toGetAsyncEnumerator
, and subsequentMoveNextAsync
operations respect it however it can.IAsyncEnumerator<T>.MoveNextAsync(CancellationToken)
: Viene passato un oggettoCancellationToken
a ogni singolaMoveNextAsync
chiamata.IAsyncEnumerator<T>.MoveNextAsync(CancellationToken)
: You pass aCancellationToken
to each individualMoveNextAsync
call.- 1 && 2: è possibile incorporare nel enumeratore/enumeratore
CancellationToken
e passareCancellationToken
aGetAsyncEnumerator
.1 && 2: You both embedCancellationToken
s into your enumerable/enumerator and passCancellationToken
s intoGetAsyncEnumerator
. - 1 && 3: è possibile incorporare nel enumeratore/enumeratore
CancellationToken
e passareCancellationToken
aMoveNextAsync
.1 && 3: You both embedCancellationToken
s into your enumerable/enumerator and passCancellationToken
s intoMoveNextAsync
.
Dal punto di vista puramente teorico, (5) è il più affidabile, in quanto (a) l' MoveNextAsync
accettazione di un oggetto CancellationToken
consente il controllo più granulare sugli elementi annullati e (b) CancellationToken
è solo qualsiasi altro tipo che può essere passato come argomento negli iteratori, incorporati in tipi arbitrari e così via.From a purely theoretical perspective, (5) is the most robust, in that (a) MoveNextAsync
accepting a CancellationToken
enables the most fine-grained control over what's canceled, and (b) CancellationToken
is just any other type that can passed as an argument into iterators, embedded in arbitrary types, etc.
Tuttavia, esistono diversi problemi con questo approccio:However, there are multiple problems with that approach:
- In che modo un oggetto viene
CancellationToken
passato perGetAsyncEnumerator
renderlo nel corpo dell'iteratore?How does aCancellationToken
passed toGetAsyncEnumerator
make it into the body of the iterator? È possibile esporre una nuovaiterator
parola chiave che è possibile punteggiare per ottenere l'accesso all'oggettoCancellationToken
passato aGetEnumerator
. Tuttavia, a) si tratta di una grande quantità di macchinari aggiuntivi, b) che si tratta di un cittadino di prima classe e c) il 99% caso sembrerebbe lo stesso codice che chiama un iteratore e chiamaGetAsyncEnumerator
su di esso, nel qual caso può semplicemente passareCancellationToken
come argomento al metodo.We could expose a newiterator
keyword that you could dot off of to get access to theCancellationToken
passed toGetEnumerator
, but a) that's a lot of additional machinery, b) we're making it a very first-class citizen, and c) the 99% case would seem to be the same code both calling an iterator and callingGetAsyncEnumerator
on it, in which case it can just pass theCancellationToken
as an argument into the method. - In che modo un oggetto viene
CancellationToken
passato perMoveNextAsync
entrare nel corpo del metodo?How does aCancellationToken
passed toMoveNextAsync
get into the body of the method? Questo è ancora peggio, come se venisse esposto da uniterator
oggetto locale, il suo valore potrebbe variare tra await, il che significa che il codice registrato con il token deve annullare la registrazione prima di attendere e quindi ripetere la registrazione dopo; è anche potenzialmente piuttosto costoso dover eseguire tale operazione di registrazione e annullamento della registrazione in ogniMoveNextAsync
chiamata, indipendentemente dal fatto che sia implementato dal compilatore in un iteratore o da uno sviluppatore manualmente.This is even worse, as if it's exposed off of aniterator
local object, its value could change across awaits, which means any code that registered with the token would need to unregister from it prior to awaits and then re-register after; it's also potentially quite expensive to need to do such registering and unregistering in everyMoveNextAsync
call, regardless of whether implemented by the compiler in an iterator or by a developer manually. - In che modo uno sviluppatore Annulla un
foreach
ciclo?How does a developer cancel aforeach
loop? Se questa operazione viene eseguita assegnando un oggettoCancellationToken
a un enumeratore/enumeratore, ovvero a), è necessario supportare l'espressione diforeach
enumeratori, che li eleva a essere cittadini di prima classe, a questo punto è necessario iniziare a pensare a un ecosistema basato su enumeratori (ad esempio metodi LINQ) o b) è necessario incorporare l'oggettoCancellationToken
nell'enumerabile con unWithCancellation
metodo di estensioneIAsyncEnumerable<T>
che archivia il token fornito e quindi passarlo all'enumerabile di cui è stato eseguito il wrappedGetAsyncEnumerator
quandoGetAsyncEnumerator
viene richiamato l'oggetto nello struct restituito (ignorando tale token).If it's done by giving aCancellationToken
to an enumerable/enumerator, then either a) we need to supportforeach
'ing over enumerators, which raises them to being first-class citizens, and now you need to start thinking about an ecosystem built up around enumerators (e.g. LINQ methods) or b) we need to embed theCancellationToken
in the enumerable anyway by having someWithCancellation
extension method off ofIAsyncEnumerable<T>
that would store the provided token and then pass it into the wrapped enumerable'sGetAsyncEnumerator
when theGetAsyncEnumerator
on the returned struct is invoked (ignoring that token). In alternativa, è possibile usare solo l'oggettoCancellationToken
nel corpo del ciclo foreach.Or, you can just use theCancellationToken
you have in the body of the foreach. - Se/quando sono supportati i genericità delle query, in che modo l'oggetto
CancellationToken
fornito aGetEnumerator
oMoveNextAsync
viene passato a ogni clausola?If/when query comprehensions are supported, how would theCancellationToken
supplied toGetEnumerator
orMoveNextAsync
be passed into each clause? Il modo più semplice è semplicemente la clausola per la relativa acquisizione. a quel punto, qualsiasi token passato aGetAsyncEnumerator
/MoveNextAsync
viene ignorato.The easiest way would simply be for the clause to capture it, at which point whatever token is passed toGetAsyncEnumerator
/MoveNextAsync
is ignored.
È stata consigliata una versione precedente di questo documento (1), ma è stato passato a (4).An earlier version of this document recommended (1), but we since switched to (4).
I due problemi principali con (1):The two main problems with (1):
- i produttori di Enumerable annullabili devono implementare alcuni standard e possono sfruttare il supporto del compilatore solo per gli iteratori asincroni per implementare un
IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken)
metodo.producers of cancellable enumerables have to implement some boilerplate, and can only leverage the compiler's support for async-iterators to implement aIAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken)
method. - è probabile che molti produttori debbano semplicemente aggiungere un
CancellationToken
parametro alla propria firma enumerabile asincrona, in modo da impedire ai consumer di passare il token di annullamento desiderato quando viene assegnato unIAsyncEnumerable
tipo.it is likely that many producers would be tempted to just add aCancellationToken
parameter to their async-enumerable signature instead, which will prevent consumers from passing the cancellation token they want when they are given anIAsyncEnumerable
type.
Esistono due scenari principali di utilizzo:There are two main consumption scenarios:
await foreach (var i in GetData(token)) ...
dove il consumer chiama il metodo async-iterator,await foreach (var i in GetData(token)) ...
where the consumer calls the async-iterator method,await foreach (var i in givenIAsyncEnumerable.WithCancellation(token)) ...
posizione in cui il consumer gestisce una determinataIAsyncEnumerable
istanza.await foreach (var i in givenIAsyncEnumerable.WithCancellation(token)) ...
where the consumer deals with a givenIAsyncEnumerable
instance.
Si ritiene che un compromesso ragionevole per supportare entrambi gli scenari in modo che sia utile per i producer e i consumer di flussi asincroni consiste nell'usare un parametro con annotazioni specifico nel metodo asincrono-iteratore.We find that a reasonable compromise to support both scenarios in a way that is convenient for both producers and consumers of async-streams is to use a specially annotated parameter in the async-iterator method. L' [EnumeratorCancellation]
attributo viene utilizzato a questo scopo.The [EnumeratorCancellation]
attribute is used for this purpose. L'inserimento di questo attributo su un parametro indica al compilatore che se un token viene passato al GetAsyncEnumerator
metodo, il token deve essere utilizzato al posto del valore passato originariamente per il parametro.Placing this attribute on a parameter tells the compiler that if a token is passed to the GetAsyncEnumerator
method, that token should be used instead of the value originally passed for the parameter.
Considerare IAsyncEnumerable<int> GetData([EnumeratorCancellation] CancellationToken token = default)
.Consider IAsyncEnumerable<int> GetData([EnumeratorCancellation] CancellationToken token = default)
. L'implementatore di questo metodo può semplicemente utilizzare il parametro nel corpo del metodo.The implementer of this method can simply use the parameter in the method body. Il consumer può usare i modelli di consumo indicati in precedenza:The consumer can use either consumption patterns above:
- Se si usa
GetData(token)
, il token viene salvato in Async-Enumerable e verrà usato nell'iterazione.if you useGetData(token)
, then the token is saved into the async-enumerable and will be used in iteration, - Se si usa
givenIAsyncEnumerable.WithCancellation(token)
, il token passato a sostituiràGetAsyncEnumerator
tutti i token salvati in Async-Enumerable.if you usegivenIAsyncEnumerable.WithCancellation(token)
, then the token passed toGetAsyncEnumerator
will supersede any token saved in the async-enumerable.
foreachforeach
foreach
verrà ampliato per supportare oltre IAsyncEnumerable<T>
al supporto esistente per IEnumerable<T>
.foreach
will be augmented to support IAsyncEnumerable<T>
in addition to its existing support for IEnumerable<T>
. E supporterà l'equivalente di IAsyncEnumerable<T>
come modello se i membri rilevanti vengono esposti pubblicamente, eseguendo il fallback all'utilizzo diretto dell'interfaccia in caso contrario, per consentire le estensioni basate su struct che evitano l'allocazione e l'utilizzo di awaitable alternativi come tipo restituito di MoveNextAsync
e DisposeAsync
.And it will support the equivalent of IAsyncEnumerable<T>
as a pattern if the relevant members are exposed publicly, falling back to using the interface directly if not, in order to enable struct-based extensions that avoid allocating as well as using alternative awaitables as the return type of MoveNextAsync
and DisposeAsync
.
SintassiSyntax
Usando la sintassi: Using the syntax:
foreach (var i in enumerable)
C# continuerà a trattare enumerable
come enumerabile sincrono, in modo che anche se espone le API appropriate per gli Enumerable asincroni (esponendo il modello o implementando l'interfaccia), verranno considerate solo le API sincrone.C# will continue to treat enumerable
as a synchronous enumerable, such that even if it exposes the relevant APIs for async enumerables (exposing the pattern or implementing the interface), it will only consider the synchronous APIs.
Per forzare foreach
invece considerare solo le API asincrone, await
viene inserito come segue:To force foreach
to instead only consider the asynchronous APIs, await
is inserted as follows:
await foreach (var i in enumerable)
Non viene fornita alcuna sintassi che supporti l'uso delle API Async o Sync. lo sviluppatore deve scegliere in base alla sintassi utilizzata.No syntax would be provided that would support using either the async or the sync APIs; the developer must choose based on the syntax used.
Opzioni scartate considerate:Discarded options considered:
foreach (var i in await enumerable)
: Si tratta di una sintassi già valida e la modifica del significato sarebbe una modifica di rilievo.foreach (var i in await enumerable)
: This is already valid syntax, and changing its meaning would be a breaking change. Ciò significa cheawait
enumerable
è possibile recuperare un elemento iterable in modo sincrono e quindi eseguire l'iterazione in modo sincrono.This means toawait
theenumerable
, get back something synchronously iterable from it, and then synchronously iterate through that.foreach (var i await in enumerable)
,foreach (var await i in enumerable)
,foreach (await var i in enumerable)
: Questi tutti indicano che è in attesa dell'elemento successivo, ma sono presenti altre attese in foreach, in particolare se l'enumerabile è un oggettoIAsyncDisposable
, verrà eseguita laawait
sua eliminazione asincrona.foreach (var i await in enumerable)
,foreach (var await i in enumerable)
,foreach (await var i in enumerable)
: These all suggest that we're awaiting the next item, but there are other awaits involved in foreach, in particular if the enumerable is anIAsyncDisposable
, we will beawait
'ing its async disposal. Questo await è come ambito di foreach anziché per ogni singolo elemento, quindi laawait
parola chiave merita di essere alforeach
livello.That await is as the scope of the foreach rather than for each individual element, and thus theawait
keyword deserves to be at theforeach
level. Inoltre, la relativa associazione aforeach
fornisce un modo per descrivere l'oggettoforeach
con un termine diverso, ad esempio "await foreach".Further, having it associated with theforeach
gives us a way to describe theforeach
with a different term, e.g. a "await foreach". Tuttavia, ancora più importante, è necessario considerareforeach
la sintassi allo stesso tempo dellausing
sintassi, in modo che rimangano coerenti tra loro edusing (await ...)
è già una sintassi valida.But more importantly, there's value in consideringforeach
syntax at the same time asusing
syntax, so that they remain consistent with each other, andusing (await ...)
is already valid syntax.foreach await (var i in enumerable)
Tenere comunque presente quanto segue:Still to consider:
foreach
Attualmente non supporta l'iterazione in un enumeratore.foreach
today does not support iterating through an enumerator. Si prevede che sia più comune che siaIAsyncEnumerator<T>
passato, quindi è possibile che si stia tentando di supportareawait foreach
sia con che conIAsyncEnumerable<T>
IAsyncEnumerator<T>
.We expect it will be more common to haveIAsyncEnumerator<T>
s handed around, and thus it's tempting to supportawait foreach
with bothIAsyncEnumerable<T>
andIAsyncEnumerator<T>
. Tuttavia, una volta aggiunto tale supporto, viene introdotta la domanda cheIAsyncEnumerator<T>
indica se è un cittadino di prima classe e se è necessario disporre di overload di combinatori che operano su enumeratori in aggiunta a enumeratori?But once we add such support, it introduces the question of whetherIAsyncEnumerator<T>
is a first-class citizen, and whether we need to have overloads of combinators that operate on enumerators in addition to enumerables? Si vuole incoraggiare i metodi per restituire gli enumeratori anziché gli enumerabili?Do we want to encourage methods to return enumerators rather than enumerables? È necessario continuare a illustrare questo problema.We should continue to discuss this. Se si decide di non supportarlo, potrebbe essere necessario introdurre un metodo di estensionepublic static IAsyncEnumerable<T> AsEnumerable<T>(this IAsyncEnumerator<T> enumerator);
che consenta a un enumeratore di essere ancoraforeach
.If we decide we don't want to support it, we might want to introduce an extension methodpublic static IAsyncEnumerable<T> AsEnumerable<T>(this IAsyncEnumerator<T> enumerator);
that would allow an enumerator to still beforeach
'd. Se si decide di supportarla, è necessario decidere se l'oggettoawait foreach
sarà responsabile della chiamata all'DisposeAsync
enumeratore e se la risposta è probabilmente "No, il controllo sull'eliminazione deve essere gestito da chiunque abbia chiamatoGetEnumerator
".If we decide we do want to support it, we'll need to also decide on whether theawait foreach
would be responsible for callingDisposeAsync
on the enumerator, and the answer is likely "no, control over disposal should be handled by whoever calledGetEnumerator
."
Compilazione basata su modelliPattern-based Compilation
Il compilatore eseguirà l'associazione alle API basate su modelli, se esistenti, preferendole tramite l'interfaccia (il modello può essere soddisfatto con i metodi di istanza o i metodi di estensione).The compiler will bind to the pattern-based APIs if they exist, preferring those over using the interface (the pattern may be satisfied with instance methods or extension methods). I requisiti per il modello sono:The requirements for the pattern are:
- L'enumerabile deve esporre un
GetAsyncEnumerator
metodo che può essere chiamato senza argomenti e che restituisce un enumeratore che soddisfa il modello pertinente.The enumerable must expose aGetAsyncEnumerator
method that may be called with no arguments and that returns an enumerator that meets the relevant pattern. - L'enumeratore deve esporre un
MoveNextAsync
metodo che può essere chiamato senza argomenti e che restituisce un elemento che può essereawait
ed eGetResult()
il cui restituiscebool
.The enumerator must expose aMoveNextAsync
method that may be called with no arguments and that returns something which may beawait
ed and whoseGetResult()
returns abool
. - L'enumeratore deve inoltre esporre la
Current
proprietà il cui Getter restituisce un oggettoT
che rappresenta il tipo di dati enumerati.The enumerator must also exposeCurrent
property whose getter returns aT
representing the kind of data being enumerated. - L'enumeratore può facoltativamente esporre un
DisposeAsync
metodo che può essere richiamato senza argomenti e che restituisce un elemento che può essere edawait
e il cuiGetResult()
restituiscevoid
.The enumerator may optionally expose aDisposeAsync
method that may be invoked with no arguments and that returns something that can beawait
ed and whoseGetResult()
returnsvoid
.
Questo codice:This code:
var enumerable = ...;
await foreach (T item in enumerable)
{
...
}
viene convertito in equivalente a:is translated to the equivalent of:
var enumerable = ...;
var enumerator = enumerable.GetAsyncEnumerator();
try
{
while (await enumerator.MoveNextAsync())
{
T item = enumerator.Current;
...
}
}
finally
{
await enumerator.DisposeAsync(); // omitted, along with the try/finally, if the enumerator doesn't expose DisposeAsync
}
Se il tipo iterato non espone il modello corretto, verranno usate le interfacce.If the iterated type doesn't expose the right pattern, the interfaces will be used.
ConfigureAwaitConfigureAwait
Questa compilazione basata su modelli consentirà ConfigureAwait
di essere usata su tutti gli await, tramite un ConfigureAwait
metodo di estensione:This pattern-based compilation will allow ConfigureAwait
to be used on all of the awaits, via a ConfigureAwait
extension method:
await foreach (T item in enumerable.ConfigureAwait(false))
{
...
}
Questa operazione sarà basata sui tipi che si aggiungeranno anche a .NET, probabilmente System.Threading.Tasks.Extensions.dll:This will be based on types we'll add to .NET as well, likely to System.Threading.Tasks.Extensions.dll:
// Approximate implementation, omitting arg validation and the like
namespace System.Threading.Tasks
{
public static class AsyncEnumerableExtensions
{
public static ConfiguredAsyncEnumerable<T> ConfigureAwait<T>(this IAsyncEnumerable<T> enumerable, bool continueOnCapturedContext) =>
new ConfiguredAsyncEnumerable<T>(enumerable, continueOnCapturedContext);
public struct ConfiguredAsyncEnumerable<T>
{
private readonly IAsyncEnumerable<T> _enumerable;
private readonly bool _continueOnCapturedContext;
internal ConfiguredAsyncEnumerable(IAsyncEnumerable<T> enumerable, bool continueOnCapturedContext)
{
_enumerable = enumerable;
_continueOnCapturedContext = continueOnCapturedContext;
}
public ConfiguredAsyncEnumerator<T> GetAsyncEnumerator() =>
new ConfiguredAsyncEnumerator<T>(_enumerable.GetAsyncEnumerator(), _continueOnCapturedContext);
public struct Enumerator
{
private readonly IAsyncEnumerator<T> _enumerator;
private readonly bool _continueOnCapturedContext;
internal Enumerator(IAsyncEnumerator<T> enumerator, bool continueOnCapturedContext)
{
_enumerator = enumerator;
_continueOnCapturedContext = continueOnCapturedContext;
}
public ConfiguredValueTaskAwaitable<bool> MoveNextAsync() =>
_enumerator.MoveNextAsync().ConfigureAwait(_continueOnCapturedContext);
public T Current => _enumerator.Current;
public ConfiguredValueTaskAwaitable DisposeAsync() =>
_enumerator.DisposeAsync().ConfigureAwait(_continueOnCapturedContext);
}
}
}
}
Si noti che questo approccio non consentirà ConfigureAwait
di usare con Enumerable basati su modelli, tuttavia, anche in questo caso, è possibile che ConfigureAwait
sia esposto solo come estensione in Task
/ Task<T>
/ ValueTask
/ ValueTask<T>
e non può essere applicato a elementi awaitable arbitrari, perché è opportuno solo quando viene applicato alle attività (controlla un comportamento implementato nel supporto di continuazione dell'attività) e pertanto non ha senso quando si usa un modello in cui le operazioni awaitable potrebbero non essere attività.Note that this approach will not enable ConfigureAwait
to be used with pattern-based enumerables, but then again it's already the case that the ConfigureAwait
is only exposed as an extension on Task
/Task<T>
/ValueTask
/ValueTask<T>
and can't be applied to arbitrary awaitable things, as it only makes sense when applied to Tasks (it controls a behavior implemented in Task's continuation support), and thus doesn't make sense when using a pattern where the awaitable things may not be tasks. Tutti gli utenti che restituiscono elementi awaitable possono fornire un comportamento personalizzato in scenari avanzati.Anyone returning awaitable things can provide their own custom behavior in such advanced scenarios.
Se è possibile trovare un modo per supportare una soluzione a livello di ambito o di assembly ConfigureAwait
, questa operazione non è necessaria.(If we can come up with some way to support a scope- or assembly-level ConfigureAwait
solution, then this won't be necessary.)
Iteratori asincroniAsync Iterators
Il linguaggio/compilatore supporterà le istanze di IAsyncEnumerable<T>
e, IAsyncEnumerator<T>
oltre a essere utilizzate.The language / compiler will support producing IAsyncEnumerable<T>
s and IAsyncEnumerator<T>
s in addition to consuming them. Attualmente il linguaggio supporta la scrittura di un iteratore come:Today the language supports writing an iterator like:
static IEnumerable<int> MyIterator()
{
try
{
for (int i = 0; i < 100; i++)
{
Thread.Sleep(1000);
yield return i;
}
}
finally
{
Thread.Sleep(200);
Console.WriteLine("finally");
}
}
ma await
non può essere usato nel corpo di questi iteratori.but await
can't be used in the body of these iterators. Il supporto viene aggiunto.We will add that support.
SintassiSyntax
Il supporto del linguaggio esistente per gli iteratori deduce la natura dell'iteratore del metodo a seconda che contenga una o più istanze di yield
.The existing language support for iterators infers the iterator nature of the method based on whether it contains any yield
s. Lo stesso valore sarà vero per gli iteratori asincroni.The same will be true for async iterators. Tali iteratori asincroni saranno delimitati e differenziati dagli iteratori sincroni tramite async
l'aggiunta alla firma e devono quindi avere anche IAsyncEnumerable<T>
o IAsyncEnumerator<T>
come tipo restituito.Such async iterators will be demarcated and differentiated from synchronous iterators via adding async
to the signature, and must then also have either IAsyncEnumerable<T>
or IAsyncEnumerator<T>
as its return type. Ad esempio, l'esempio precedente potrebbe essere scritto come iteratore asincrono, come indicato di seguito:For example, the above example could be written as an async iterator as follows:
static async IAsyncEnumerable<int> MyIterator()
{
try
{
for (int i = 0; i < 100; i++)
{
await Task.Delay(1000);
yield return i;
}
}
finally
{
await Task.Delay(200);
Console.WriteLine("finally");
}
}
Alternative considerate:Alternatives considered:
- Non usando
async
nella firma: l'uso diasync
è probabilmente tecnicamente richiesto dal compilatore, perché lo usa per determinare seawait
è valido in tale contesto.Not usingasync
in the signature: Usingasync
is likely technically required by the compiler, as it uses it to determine whetherawait
is valid in that context. Tuttavia, anche se non è obbligatorio, abbiamo stabilito cheawait
può essere usato solo nei metodi contrassegnati comeasync
e sembra importante mantenerne la coerenza.But even if it's not required, we've established thatawait
may only be used in methods marked asasync
, and it seems important to keep the consistency. - Abilitazione di generatori
IAsyncEnumerable<T>
personalizzati per: si tratta di un elemento che è possibile esaminare per il futuro, ma il meccanismo è complesso e non è supportato per le controparti sincrone.Enabling custom builders forIAsyncEnumerable<T>
: That's something we could look at for the future, but the machinery is complicated and we don't support that for the synchronous counterparts. - Con una
iterator
parola chiave nella firma: gli iteratori asincroni userannoasync iterator
nella firma eyield
possono essere usati solo neiasync
metodi che includonoiterator
;iterator
verrebbe quindi reso facoltativo negli iteratori sincroni.Having aniterator
keyword in the signature: Async iterators would useasync iterator
in the signature, andyield
could only be used inasync
methods that includediterator
;iterator
would then be made optional on synchronous iterators. A seconda della prospettiva, questo ha il vantaggio di renderla molto chiara dalla firma del metodo, indipendentemente dal fatto cheyield
sia consentita e se il metodo è effettivamente destinato a restituire istanze di tipoIAsyncEnumerable<T>
anziché alla produzione del compilatore a seconda che il codice usiyield
o meno.Depending on your perspective, this has the benefit of making it very clear by the signature of the method whetheryield
is allowed and whether the method is actually meant to return instances of typeIAsyncEnumerable<T>
rather than the compiler manufacturing one based on whether the code usesyield
or not. Ma è diverso dagli iteratori sincroni, che non possono essere eseguiti per richiederne uno.But it is different from synchronous iterators, which don't and can't be made to require one. Inoltre, alcuni sviluppatori non amano la sintassi aggiuntiva.Plus some developers don't like the extra syntax. Se la progettazione è stata rilasciata da zero, è probabile che questa operazione sia necessaria, ma a questo punto è molto più utile mantenere gli iteratori asincroni vicini agli iteratori di sincronizzazione.If we were designing it from scratch, we'd probably make this required, but at this point there's much more value in keeping async iterators close to sync iterators.
LINQLINQ
Sono presenti più di ~ 200 overload di metodi sulla System.Linq.Enumerable
classe, tutti che funzionano in termini di IEnumerable<T>
; alcuni di IEnumerable<T>
essi accettano, alcuni di essi producono IEnumerable<T>
e molti eseguono entrambi.There are over ~200 overloads of methods on the System.Linq.Enumerable
class, all of which work in terms of IEnumerable<T>
; some of these accept IEnumerable<T>
, some of them produce IEnumerable<T>
, and many do both. L'aggiunta del supporto LINQ per IAsyncEnumerable<T>
comporterebbe probabilmente la duplicazione di tutti questi overload, per un altro ~ 200.Adding LINQ support for IAsyncEnumerable<T>
would likely entail duplicating all of these overloads for it, for another ~200. Poiché IAsyncEnumerator<T>
probabilmente è più comune come entità autonoma nel mondo asincrono rispetto al IEnumerator<T>
mondo sincrono, potrebbe essere necessario un altro ~ 200 overload che funzionano con IAsyncEnumerator<T>
.And since IAsyncEnumerator<T>
is likely to be more common as a standalone entity in the asynchronous world than IEnumerator<T>
is in the synchronous world, we could potentially need another ~200 overloads that work with IAsyncEnumerator<T>
. Inoltre, un numero elevato di overload riguarda i predicati (ad esempio Where
, che accetta un Func<T, bool>
) e potrebbe essere auspicabile avere IAsyncEnumerable<T>
Overload basati su che gestiscono predicati sincroni e asincroni (ad esempio, Func<T, ValueTask<bool>>
oltre a Func<T, bool>
).Plus, a large number of the overloads deal with predicates (e.g. Where
that takes a Func<T, bool>
), and it may be desirable to have IAsyncEnumerable<T>
-based overloads that deal with both synchronous and asynchronous predicates (e.g. Func<T, ValueTask<bool>>
in addition to Func<T, bool>
). Sebbene non sia applicabile a tutti i nuovi overload di ~ 400, un calcolo approssimativo è che sarebbe applicabile a metà, ovvero un altro ~ 200 Overloads, per un totale di ~ 600 nuovi metodi.While this isn't applicable to all of the now ~400 new overloads, a rough calculation is that it'd be applicable to half, which means another ~200 overloads, for a total of ~600 new methods.
Si tratta di un numero sbalorditivo di API, con la possibilità di ancora di più quando vengono prese in considerazione librerie di estensione come Interactive Extensions (IX).That is a staggering number of APIs, with the potential for even more when extension libraries like Interactive Extensions (Ix) are considered. Ma IX dispone già di un'implementazione di molti di questi e non sembra essere un ottimo motivo per duplicare il lavoro; è invece consigliabile aiutare la community a migliorare IX e consigliarla quando gli sviluppatori vogliono usare LINQ con IAsyncEnumerable<T>
.But Ix already has an implementation of many of these, and there doesn't seem to be a great reason to duplicate that work; we should instead help the community improve Ix and recommend it for when developers want to use LINQ with IAsyncEnumerable<T>
.
Esiste anche il problema della sintassi di comprensione delle query.There is also the issue of query comprehension syntax. La natura basata su modelli dei tipi di genericità delle query consente loro di "semplicemente lavorare" con alcuni operatori, ad esempio se IX fornisce i metodi seguenti:The pattern-based nature of query comprehensions would allow them to "just work" with some operators, e.g. if Ix provides the following methods:
public static IAsyncEnumerable<TResult> Select<TSource, TResult>(this IAsyncEnumerable<TSource> source, Func<TSource, TResult> func);
public static IAsyncEnumerable<T> Where(this IAsyncEnumerable<T> source, Func<T, bool> func);
il codice C# sarà quindi "solo funzionante":then this C# code will "just work":
IAsyncEnumerable<int> enumerable = ...;
IAsyncEnumerable<int> result = from item in enumerable
where item % 2 == 0
select item * 2;
Tuttavia, non esiste alcuna sintassi di comprensione delle query che supporti await
l'uso di nelle clausole, pertanto se si aggiunge IX, ad esempio:However, there is no query comprehension syntax that supports using await
in the clauses, so if Ix added, for example:
public static IAsyncEnumerable<TResult> Select<TSource, TResult>(this IAsyncEnumerable<TSource> source, Func<TSource, ValueTask<TResult>> func);
quindi, si tratta di un "solo lavoro":then this would "just work":
IAsyncEnumerable<string> result = from url in urls
where item % 2 == 0
select SomeAsyncMethod(item);
async ValueTask<int> SomeAsyncMethod(int item)
{
await Task.Yield();
return item * 2;
}
Tuttavia, non esiste alcun modo per scriverlo con l' await
inline nella select
clausola.but there'd be no way to write it with the await
inline in the select
clause. Come sforzo separato, è possibile esaminare async { ... }
l'aggiunta di espressioni al linguaggio, a questo punto è possibile consentirne l'uso nella comprensione delle query e il precedente può invece essere scritto come segue:As a separate effort, we could look into adding async { ... }
expressions to the language, at which point we could allow them to be used in query comprehensions and the above could instead be written as:
IAsyncEnumerable<int> result = from item in enumerable
where item % 2 == 0
select async
{
await Task.Yield();
return item * 2;
};
o per abilitare l' await
utilizzo diretto in espressioni, ad esempio mediante il supporto di async from
.or to enabling await
to be used directly in expressions, such as by supporting async from
. Tuttavia, è improbabile che una progettazione influisca sul resto del set di funzionalità un modo o sull'altro e che non si tratta di un aspetto particolarmente importante per investire in questo momento, quindi la proposta è di non eseguire alcuna operazione aggiuntiva in questo momento.However, it's unlikely a design here would impact the rest of the feature set one way or the other, and this isn't a particularly high-value thing to invest in right now, so the proposal is to do nothing additional here right now.
Integrazione con altri Framework asincroniIntegration with other asynchronous frameworks
L'integrazione con IObservable<T>
e altri Framework asincroni (ad esempio, i flussi reattivi) viene eseguita a livello di libreria anziché a livello di linguaggio.Integration with IObservable<T>
and other asynchronous frameworks (e.g. reactive streams) would be done at the library level rather than at the language level. È possibile, ad esempio, che tutti i dati di un oggetto IAsyncEnumerator<T>
possano essere pubblicati in un IObserver<T>
semplicemente await foreach
"ING over the enumerator OnNext
" e "ing the data to the Observer", pertanto AsObservable<T>
è possibile un metodo di estensione.For example, all of the data from an IAsyncEnumerator<T>
can be published to an IObserver<T>
simply by await foreach
'ing over the enumerator and OnNext
'ing the data to the observer, so an AsObservable<T>
extension method is possible. L'utilizzo IObservable<T>
di un in un await foreach
richiede il buffering dei dati (nel caso in cui venga eseguito il push di un altro elemento mentre l'elemento precedente è ancora in fase di elaborazione), ma è possibile implementare facilmente un adattatore push pull per consentire il pull di un oggetto IObservable<T>
con un oggetto IAsyncEnumerator<T>
.Consuming an IObservable<T>
in a await foreach
requires buffering the data (in case another item is pushed while the previous item is still being processing), but such a push-pull adapter can easily be implemented to enable an IObservable<T>
to be pulled from with an IAsyncEnumerator<T>
. Via. RX/IX fornisce già prototipi di tali implementazioni e librerie come https://github.com/dotnet/corefx/tree/master/src/System.Threading.Channels forniscono vari tipi di strutture di dati di buffering.Etc. Rx/Ix already provide prototypes of such implementations, and libraries like https://github.com/dotnet/corefx/tree/master/src/System.Threading.Channels provide various kinds of buffering data structures. In questa fase non è necessario che il linguaggio sia occupato.The language need not be involved at this stage.