異步數據流
注意
本文是功能規格。 規格可作為功能的設計檔。 其中包含建議的規格變更,以及功能設計和開發期間所需的資訊。 這些文章會發佈,直到提議的規格變更完成並併併入目前的ECMA規格為止。
功能規格與已完成實作之間可能有一些差異。 這些差異是在的相關
您可以在介紹 規格的文章中深入了解將功能規範納入 C# 語言標準的過程。
冠軍問題:https://github.com/dotnet/csharplang/issues/43
總結
C# 支援反覆運算器方法和異步方法,但不支援同時為反覆運算器和異步方法的方法。 我們應該透過允許 await
在新形式的 async
迭代器中使用來修正此問題,此迭代器將傳回 IAsyncEnumerable<T>
或 IAsyncEnumerator<T>
,而不是 IEnumerable<T>
或 IEnumerator<T>
,並且可以在新的 IAsyncEnumerable<T>
中消耗 await foreach
。
IAsyncDisposable
介面也可用來啟用異步清除。
相關討論
詳細設計
介面
IAsyncDisposable
很多關於 IAsyncDisposable
(例如 https://github.com/dotnet/roslyn/issues/114)以及這是否是個好主意的討論已經進行過。 不過,新增支援異步迭代器是必要的概念。 由於 finally
區塊可能包含 await
,且 finally
區塊必須在處置迭代器時執行,因此我們需要非同步處置。 在清除資源時通常也相當有用,可能需要任何時間,例如關閉檔案(需要清除)、取消註冊回呼,並提供一種方式來知道何時完成取消註冊等等。
下列介面會新增至核心 .NET 連結庫(例如 System.Private.CoreLib / System.Runtime):
namespace System
{
public interface IAsyncDisposable
{
ValueTask DisposeAsync();
}
}
如同 Dispose
,多次呼叫 DisposeAsync
是可以接受的,而第一次之後的呼叫應該視為無操作,傳回同步完成的成功任務(不過,DisposeAsync
不需要具線程安全性,也不需要支持並行呼叫)。 此外,型別可以同時實作 IDisposable
和 IAsyncDisposable
,而且如果實作,同樣可以接受叫用 Dispose
,然後 DisposeAsync
反之亦然,但只有第一個應該有意義,後續叫用兩者都應該是 nop。 因此,如果某類型確實實作了這兩者,建議使用者根據內容只呼叫一次更相關的方法:在同步上下文中呼叫 Dispose
,在異步上下文中則呼叫 DisposeAsync
。
(IAsyncDisposable
如何與 using
互動屬於單獨的討論。至於它與 foreach
的互動,本文會在稍後說明。)
考慮的替代方案:
-
DisposeAsync
接受CancellationToken
:理論上,任何異步操作都可以被取消,但處置涉及清理、關閉事物、釋放資源等,通常這些不應被取消;即使工作已被取消,清理仍然非常重要。 造成實際工作取消的相同CancellationToken
通常會是傳遞至DisposeAsync
的相同令牌,因此DisposeAsync
毫無價值,因為取消工作會導致DisposeAsync
成為 no-op。 如果有人想要避免因等待處置而被封鎖,他們可以避免等待ValueTask
的結果,或只等待它一段時間。 - 傳回
:現在非泛型 存在,而且可以從 建構,從 傳回 ,可讓現有的物件重複使用為代表最終異步完成 的承諾,並在 異步完成的情況下儲存 配置。 -
ConfigureAwait
設定 :雖然這類概念的公開方式對using
、foreach
和其他語言建構可能存在問題,但從介面的觀點來看,實際上並沒有執行任何await
的操作,也沒有需要設定的內容...ValueTask
的消費者可以根據需要取用它。 -
IAsyncDisposable
繼承IDisposable
:由於只應使用其中之一,因此強制型別實現兩者並無意義。 -
IDisposableAsync
而不是IAsyncDisposable
:我們一直遵循命名規則,即專案/類型名為『Async某物』,而作業則是以『Async』方式執行,因此類型有「Async」作為前置詞,而方法有「Async」作為後綴。
IAsyncEnumerable / IAsyncEnumerator
核心 .NET 連結庫會新增兩個介面:
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; }
}
}
一般耗用量(不含其他語言功能)看起來會像這樣:
IAsyncEnumerator<T> enumerator = enumerable.GetAsyncEnumerator();
try
{
while (await enumerator.MoveNextAsync())
{
Use(enumerator.Current);
}
}
finally { await enumerator.DisposeAsync(); }
已考慮捨棄的選項:
-
Task<bool> MoveNextAsync(); T current { get; }
:使用Task<bool>
支援使用快取的工作物件來表示同步和成功的MoveNextAsync
呼叫,但仍然需要進行某些配置以完成異步操作。 藉由傳回ValueTask<bool>
,我們可讓列舉值物件本身實作IValueTaskSource<bool>
,並做為從ValueTask<bool>
傳回之MoveNextAsync
的備份,進而大幅降低額外負荷。 -
ValueTask<(bool, T)> MoveNextAsync();
:不僅更難取用,而且意味著T
不能再成為共變數。 -
ValueTask<T?> TryMoveNextAsync();
:非共變數。 -
Task<T?> TryMoveNextAsync();
:沒有共變性、每次呼叫的分配等等。 -
ITask<T?> TryMoveNextAsync();
:沒有共變性、每次呼叫的分配等等。 -
ITask<(bool,T)> TryMoveNextAsync();
:沒有共變性、每次呼叫的分配等等。 -
Task<bool> TryMoveNextAsync(out T result);
:需要在作業同步傳回時設定out
結果,而不是等到作業可能在未來經過很長時間後才完成,因為那時無法傳達結果。 -
IAsyncEnumerator<T>
未實作IAsyncDisposable
:我們可以選擇分開這些。 不過,這樣做會使提案的某些其他領域變得複雜,因為程式碼必須能夠處理列舉器無法提供釋放的可能性,這使得撰寫模式化的輔助工具變得困難。 此外,列舉器通常需要進行資源釋放(例如,任何具有 finally 區塊的 C# 非同步迭代器、從網路連線列舉資料的過程等)。若不需要,則可以輕鬆地將方法僅實作為public ValueTask DisposeAsync() => default(ValueTask);
,並且只需極少的額外負擔。 - _
IAsyncEnumerator<T> GetAsyncEnumerator()
:沒有取消令牌參數。
下列小節討論未選擇的替代方案。
可行的替代方案:
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
被用在內部迴圈中,以單一介面呼叫來處理項目,只要這些項目可以同步取得。 當無法同步擷取下一個項目時,它會傳回 false,而且每當傳回 false 時,呼叫方必須隨後呼叫 WaitForNextAsync
,以便等待下一個項目可供使用或判斷永遠不會有其他項目。 一般耗用量(不含其他語言功能)看起來會像這樣:
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(); }
其優點分為兩方面:一個次要的和一個主要的:
-
Minor:允許列舉器支援多個取用者。 在某些情況下,列舉值對於支援多個並行取用者而言非常重要。 當
MoveNextAsync
和Current
是分開的,實作就無法將它們的使用達到原子性,因此無法實現該目的。 相反地,此方法提供一個單一的TryGetNext
方法,支援推動列舉器向前並取得下一個項目,因此枚舉器可以視需要啟用原子性。 不過,通過給每一個消費者從共用的列舉集合中分配一個專屬的列舉器,這類情境可能被啟用。 此外,我們不想強制每個枚舉器都支援併發使用,因為這將在大多數不需要這種功能的情況下增加顯著的額外負擔,這意味著介面的使用者通常無法以此方式依賴。 -
主要:效能。
MoveNextAsync
/Current
方式每個作業需要兩次介面呼叫,而WaitForNextAsync
/TryGetNext
的最佳情況是,大部分的迴圈同步完成,使得具有TryGetNext
的緊密的內部迴圈,每個作業只有一次介面呼叫。 在介面呼叫主宰計算的情況下,這可能會產生可測量的影響。
不過,也有非微不足道的缺點,包括在手動使用這些時,複雜度明顯增加,以及在使用它們時更容易引入 Bug。 雖然效能優勢在微基準測試中顯現,但我們並不認為它們會對絕大多數實際使用產生顯著影響。 如果證實他們符合,我們可以以高亮顯示的方式引進第二組介面。
已考慮捨棄的選項:
-
ValueTask<bool> WaitForNextAsync(); bool TryGetNext(out T result);
:out
參數不能是共變數。 這裡同樣受到了一個小影響(這是嘗試模式中普遍存在的問題),這可能會在運行時對參考類型結果造成寫屏障。
取消
支援取消的方法有數種:
-
IAsyncEnumerable<T>
/IAsyncEnumerator<T>
不受取消影響,CancellationToken
不會出現在任何地方。 取消是透過邏輯方式將CancellationToken
嵌入到可列舉和/或迭代器中,以任何適當的方式達成,例如呼叫迭代器時,將CancellationToken
當做參數傳遞至迭代器方法,並在迭代器主體中使用它,就像對任何其他參數所做的那樣。 -
IAsyncEnumerator<T>.GetAsyncEnumerator(CancellationToken)
:您將CancellationToken
傳遞給GetAsyncEnumerator
,後續的MoveNextAsync
作業會在可能的情況下尊重它。 -
IAsyncEnumerator<T>.MoveNextAsync(CancellationToken)
:您會將CancellationToken
傳遞給每個個別MoveNextAsync
呼叫。 - 1 && 2:您會將
CancellationToken
嵌入至可列舉集合/枚舉器,並將CancellationToken
傳遞至GetAsyncEnumerator
。 - 1 && 3:您將
CancellationToken
嵌入至您的可列舉項或列舉器中,並將CancellationToken
傳遞至MoveNextAsync
。
從純理論的觀點來看,(5)是最具堅實性的,因為(a)MoveNextAsync
接受 CancellationToken
允許對取消的內容進行最細緻的控制,而(b)CancellationToken
則是其他任何可以作為參數傳遞給反覆運算器的類型,或是內嵌在任意類型中的類型等。
不過,該方法有多個問題:
- 如何將
CancellationToken
傳遞到GetAsyncEnumerator
並進入迭代器的主體? 我們可以公開一個新的iterator
關鍵詞,你可以利用此關鍵詞來存取傳遞至CancellationToken
的GetEnumerator
,但 a)這涉及很多額外的操作架構,b)我們將其提升為主要功能,c)99% 情況似乎都是在調用迭代器和調用GetAsyncEnumerator
上使用相同的代碼,這種情況下,可以將CancellationToken
作為參數傳遞到該方法。 - 傳遞至
CancellationToken
的MoveNextAsync
如何進入到方法體中? 更糟的是,好像它從iterator
本機對象公開,其值可能會在 await 之間變更,這表示任何向令牌註冊的程式代碼都必須在等候之前從它取消註冊,然後在之後重新註冊;不論編譯程式是在反覆運算器或開發人員手動實作,都需要在每個MoveNextAsync
呼叫中執行這類註冊和取消註冊,也可能會相當昂貴。 - 開發人員如何取消
foreach
迴圈? 如果藉由將CancellationToken
提供給可列舉/列舉器來完成,那麼,我們要麼 a)需要支援在列舉器上進行foreach
操作,使其成為一等公民,並且您需要開始考慮圍繞列舉器建立的生態系統(例如 LINQ 方法),要麼 b)我們需要在可列舉中內嵌CancellationToken
,藉由在WithCancellation
上的某個IAsyncEnumerable<T>
擴充方法來儲存提供的令牌,然後在傳回結構的GetAsyncEnumerator
被調用時將其傳遞給包裝的可列舉的GetAsyncEnumerator
(忽略該令牌)。 或者,您可以只使用您在 foreach 主體中的CancellationToken
。 - 如果/當查詢理解被支持時,提供給
CancellationToken
或GetEnumerator
的MoveNextAsync
將如何被傳遞到每個子句中? 最簡單的方式只需讓子句擷取它,此時傳遞至GetAsyncEnumerator
/MoveNextAsync
的任何令牌會被忽略。
本檔先前的版本建議使用 (1),但我們自那以後切換到 (4)。
(1) 的兩個主要問題:
- 可取消的列舉項生產者必須實作一些樣板程式碼,並且只能利用編譯器對非同步迭代器的支援來實作
IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken)
方法。 - 可能許多製造者會傾向於僅將
CancellationToken
參數新增到其異步可列舉物件的簽章中,這樣在獲得IAsyncEnumerable
類型時,就能防止取用者傳遞其想要的取消令牌。
有兩個主要的消費情境:
-
await foreach (var i in GetData(token)) ...
消費者呼叫 async-iterator 方法的位置, -
await foreach (var i in givenIAsyncEnumerable.WithCancellation(token)) ...
消費者處理指定IAsyncEnumerable
實例。
我們發現,為了方便異步數據流的產生者和使用者,可以使用在異步迭代器方法中帶有註解的參數,以合理支援這兩種情境。
[EnumeratorCancellation]
屬性用於此用途。 將這個屬性放在參數上會告訴編譯程式,如果令牌傳遞至 GetAsyncEnumerator
方法,則應該使用該令牌,而不是原本為 參數傳遞的值。
請考慮 IAsyncEnumerable<int> GetData([EnumeratorCancellation] CancellationToken token = default)
。
這個方法的實作者只要在方法主體中使用 參數即可。
消費者可以使用上述任一消費模式:
- 如果您使用
GetData(token)
,則令牌會儲存到異步列舉中,並將用於反覆運算中, - 如果您使用
givenIAsyncEnumerable.WithCancellation(token)
,則傳遞至GetAsyncEnumerator
的令牌將會取代儲存在異步列舉中的任何令牌。
foreach
除了現有對 foreach
的支援之外,IAsyncEnumerable<T>
也會增強以支援 IEnumerable<T>
。 如果相關成員是公開的,則支援與 IAsyncEnumerable<T>
相同的模式;如果未公開,則直接使用介面。如此便可以啟用基於結構的擴充功能,從而避免配置,並將替代的等候物用作 MoveNextAsync
和 DisposeAsync
的回傳型別。
語法
使用語法:
foreach (var i in enumerable)
C# 會繼續將 enumerable
視為同步可列舉,因此即使它公開了非同步列舉的相關 API(公開模式或實作介面),也只會考慮同步的 API。
若要強制 foreach
改為僅考慮異步的 API,可以按如下方式插入 await
:
await foreach (var i in enumerable)
不會提供任何支援使用異步或同步 API 的語法;開發人員必須根據所使用的語法來選擇。
語義學
在編譯時間處理 await foreach
語句時,會先決定該表達式的 集合類型、列舉值類型 和 迭代類型(非常類似於 https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/statements.md#1395-the-foreach-statement)。 此判斷的步驟如下所示:
- 如果
X
的類型 是dynamic
或陣列類型,則會產生錯誤,而且不會採取任何進一步的步驟。 - 否則,請判斷類型
X
是否有適當的GetAsyncEnumerator
方法:- 使用標識碼
X
且沒有類型自變數,對類型GetAsyncEnumerator
執行成員查閱。 如果成員查閱不會產生相符專案,或產生模棱兩可,或產生不是方法群組的相符專案,請檢查可列舉的介面,如下所述。 - 使用產生的方法群組和空的自變數清單來執行多載解析。 如果多載解析沒有產生任何適用的方法、產生模棱兩可的方法,或產生單一最佳方法,但該方法為靜態或不公用,請檢查可列舉的介面,如下所示。
- 如果
E
方法的傳回型別GetAsyncEnumerator
不是類別、結構或介面類型,則會產生錯誤,而且不會採取任何進一步的步驟。 - 成員查閱會在具有標識碼
E
且沒有類型自變數的Current
上執行。 如果成員查詢未找到匹配項目,結果將是錯誤;若結果是除可讀取的公用實例屬性以外的任何項目,亦將產生錯誤,且不會採取任何進一步的步驟。 - 成員查閱會在具有標識碼
E
且沒有類型自變數的MoveNextAsync
上執行。 如果成員查閱不會產生任何相符專案,則結果為錯誤,或結果為方法群組以外的任何專案,則會產生錯誤,而且不會採取任何進一步的步驟。 - 多載解析是在具有空自變數清單的方法群組上執行。 如果多載解析結果沒有產生任何適用的方法、結果模棱兩可、或產生單一最佳方法,但該方法是靜態的或非公共的,或其返回類型無法等待至
bool
,則會產生錯誤,並且不會採取進一步的步驟。 - 集合類型是
X
,列舉值類型是E
,而反覆項目類型是Current
屬性的類型。
- 使用標識碼
- 否則,請檢查可列舉介面:
- 如果從
Tᵢ
隱含轉換成X
的所有類型IAsyncEnumerable<ᵢ>
中,有一個唯一的類型T
,因此T
不是動態的,而所有其他Tᵢ
則有從IAsyncEnumerable<T>
隱含轉換成IAsyncEnumerable<Tᵢ>
的隱含轉換,則集合類型是介面IAsyncEnumerable<T>
,列舉值類型是介面IAsyncEnumerator<T>
, 且反覆運算類型T
。 - 否則,如果有多個這類類型
T
,則會產生錯誤,而且不會採取任何進一步的步驟。
- 如果從
- 否則,會產生錯誤,而且不會採取任何進一步的步驟。
如果成功,上述步驟會明確產生集合類型 C
、列舉值類型 E
和反覆運算類型 T
。
await foreach (V v in x) «embedded_statement»
然後展開至:
{
E e = ((C)(x)).GetAsyncEnumerator();
try {
while (await e.MoveNextAsync()) {
V v = (V)(T)e.Current;
«embedded_statement»
}
}
finally {
... // Dispose e
}
}
finally
區塊的主體是根據下列步驟建構的:
- 如果類型
E
具有適當的DisposeAsync
方法:- 使用標識碼
E
且沒有類型自變數,對類型DisposeAsync
執行成員查閱。 如果成員查找未能找到匹配,或者產生不確定的結果,或者找到的匹配不是方法群組,請檢查釋放資源的介面,如下所示。 - 使用產生的方法群組和空的自變數清單來執行多載解析。 如果多載解析沒有產生任何適用的方法、造成模棱兩可,或產生單一最佳方法,但該方法為靜態或不公用,請檢查處置介面,如下所示。
- 如果無法等候
DisposeAsync
方法的傳回類型,則會產生錯誤,而且不會採取任何進一步的步驟。 -
finally
子句會展開為語意等價項:
finally { await e.DisposeAsync(); }
- 使用標識碼
- 否則,如果存在從
E
到System.IAsyncDisposable
介面的隱含轉換,則- 如果
E
是不可為 Null 的實值類型,則finally
子句會展開為語意對等的:
finally { await ((System.IAsyncDisposable)e).DisposeAsync(); }
- 否則,
finally
子句會展開為語義上的等同:
除非finally { System.IAsyncDisposable d = e as System.IAsyncDisposable; if (d != null) await d.DisposeAsync(); }
E
是實值型別,或具現化為實值型別的類型參數,則e
轉換成System.IAsyncDisposable
,則不會導致Boxing發生。
- 如果
- 否則,
finally
子句會展開為空白區塊:finally { }
ConfigureAwait
此模式型編譯會允許透過 ConfigureAwait
擴充方法,在所有 await 上使用 ConfigureAwait
:
await foreach (T item in enumerable.ConfigureAwait(false))
{
...
}
這將基於我們將新增至 .NET 的類型,很可能涉及 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 ConfiguredAsyncEnumerator<T>
{
private readonly IAsyncEnumerator<T> _enumerator;
private readonly bool _continueOnCapturedContext;
internal ConfiguredAsyncEnumerator(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);
}
}
}
}
請注意,此方法不會讓 ConfigureAwait
與基於樣式的可枚舉物件搭配使用。但即便如此,ConfigureAwait
也僅作為 Task
/Task<T>
/ValueTask
/ValueTask<T>
上的擴展方法公開,並且無法應用於任意可等候的對象,因為它只有在應用於 Task 時才有意義(它控制在 Task 的延續支援中實現的行為),因此,當使用可等候對象可能不是 Task 的模式時,並無意義。 任何傳回可等待對象的人在這類進階情境中,可以提供自己的客製化行為。
(如果我們能想出某種方法來支援範圍或元件層級 ConfigureAwait
解決方案,則不需要這樣做。
異步反覆運算器
語言/編譯程式將不僅支援使用 IAsyncEnumerable<T>
和 IAsyncEnumerator<T>
,還能支援其生產。 目前語言支援撰寫反覆運算器,例如:
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");
}
}
但是 await
不能用於這些反覆運算器的主體中。 我們將新增該支援。
語法
現有的語言支援會根據方法中是否包含任何 yield
來推斷該方法的迭代器特性。 異步反覆運算器也是如此。 這類異步反覆運算器會透過在簽章中新增 async
來與同步反覆運算器區分,並且也必須將 IAsyncEnumerable<T>
或 IAsyncEnumerator<T>
作為其傳回型別。 例如,上述範例可以撰寫為異步反覆運算器,如下所示:
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");
}
}
考慮的替代方案:
async
中使用 :編譯器可能技術上需要async
,因為它利用async
判斷 是否在該上下文中有效。 但是,即使不需要,我們也已確定await
只能在標示為async
的方法中使用,而且保持一致性似乎很重要。-
啟用
IAsyncEnumerable<T>
的自定義構建器:這是我們未來可以考慮的,但由於機械非常複雜,目前我們不支援同步對應版本。 -
iterator
中具有 關鍵詞:異步反覆運算器會在其簽名中使用async iterator
,並且yield
只能用於包含async
的iterator
方法;隨後,iterator
在同步反覆運算器中變為可選。 視您的觀點而定,這有一個優點,即透過方法簽章清楚表明是否允許yield
,以及方法之所以設計是要返回類型IAsyncEnumerable<T>
的實例,而非根據程式碼是否使用yield
而自行生成一個。 但它與同步反覆運算器不同,後者不需要這樣做,也無法要求這樣做。 此外,有些開發人員不喜歡額外的語法。 如果我們從頭設計,我們可能會將這項設為必須的,但考慮到現階段,讓異步反覆運算器的功能盡量接近同步反覆運算器更有價值。
LINQ
System.Linq.Enumerable
類別上有超過 200 個方法多載,所有方法都以 IEnumerable<T>
為基準運作;其中一些接受 IEnumerable<T>
,一些會產生 IEnumerable<T>
,而且許多方法同時執行這兩者。 針對 IAsyncEnumerable<T>
新增 LINQ 支援,可能需要為它複製所有的多載,約 200 個。 而且,由於 IAsyncEnumerator<T>
在異步世界中作為獨立實體可能比 IEnumerator<T>
在同步世界中更常見,因此我們可能需要大約另外 200 個與 IAsyncEnumerator<T>
搭配使用的多載。 此外,大量的重載涉及處理述詞(例如採用 Where
的 Func<T, bool>
),並且可能還需要有基於 IAsyncEnumerable<T>
的重載來處理同步和異步述詞(例如,除了 Func<T, ValueTask<bool>>
之外的 Func<T, bool>
)。 雖然這不適用於目前大約 400 個新多載中的所有情況,但粗略計算適用於其中的一半,這表示額外大約 200 個多載,總計大約 600 個新方法。
這是一個驚人的 API 數目,當考慮互動式延伸模組 (Ix) 等擴充連結庫時,可能會有更多的 API。 但 Ix 已經有實作其中多數功能,而且似乎沒有正當理由去重複這項工作;我們應該協助社群共同改進 Ix,並在開發者想把 LINQ 與 IAsyncEnumerable<T>
搭配使用時推薦它。
查詢語法理解的問題也存在。 查詢理解的模式化特性使得它們能夠輕鬆地與某些運算符搭配使用,例如,如果 Ix 提供下列方法:
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);
然後這個 C# 程式代碼會「正常運作」:
IAsyncEnumerable<int> enumerable = ...;
IAsyncEnumerable<int> result = from item in enumerable
where item % 2 == 0
select item * 2;
不過,沒有支援在 子句中使用 await
的查詢理解語法,因此如果新增了 Ix,例如:
public static IAsyncEnumerable<TResult> Select<TSource, TResult>(this IAsyncEnumerable<TSource> source, Func<TSource, ValueTask<TResult>> func);
然後這就會「正常運作」:
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;
}
但是沒有任何方法可以在 await
子句中將 select
內嵌進去來撰寫。 作為另一項工作,我們可以考慮將 async { ... }
表示式新增至語言,此時我們可以允許這些表達式用於查詢理解,而上述表達式可以改為撰寫為:
IAsyncEnumerable<int> result = from item in enumerable
where item % 2 == 0
select async
{
await Task.Yield();
return item * 2;
};
使 await
能夠直接在表示式中使用,例如透過支援 async from
。 不過,這裡的設計不太可能對其他功能集有太大的影響,這不是一個現在值得大量投資的高價值項目,所以建議現在不會額外採取任何行動。
與其他異步架構整合
與 IObservable<T>
和其他異步架構整合(例如反應式數據流)會在連結庫層級進行,而不是在語言層級完成。 例如,通过對列舉器進行 IAsyncEnumerator<T>
操作並將數據 IObserver<T>
給觀察者,可以將來自 await foreach
的所有數據發行至 OnNext
,從而實現 AsObservable<T>
擴展方法。 在 IObservable<T>
中取用 await foreach
需要緩衝處理數據(以防在上一個專案仍在處理時推送另一個專案),但可以輕鬆地實作這類推送提取配接器,讓 IObservable<T>
能夠搭配 IAsyncEnumerator<T>
提取。 等等,Rx/Ix 已經提供這類實作的原型,而像是 https://github.com/dotnet/corefx/tree/master/src/System.Threading.Channels 的連結庫則提供各種緩衝處理數據結構。 此階段不需要涉及語言。