共用方式為


異步數據流

注意

本文是功能規格。 規格可作為功能的設計檔。 其中包含建議的規格變更,以及功能設計和開發期間所需的資訊。 這些文章會發佈,直到提議的規格變更完成並併併入目前的ECMA規格為止。

功能規格與已完成實作之間可能有一些差異。 這些差異是在的相關 語言設計會議(LDM)注意事項中擷取的。

您可以在介紹 規格的文章中深入了解將功能規範納入 C# 語言標準的過程

冠軍問題:https://github.com/dotnet/csharplang/issues/43

總結

C# 支援反覆運算器方法和異步方法,但不支援同時為反覆運算器和異步方法的方法。 我們應該透過允許 await 在新形式的 async 迭代器中使用來修正此問題,此迭代器將傳回 IAsyncEnumerable<T>IAsyncEnumerator<T>,而不是 IEnumerable<T>IEnumerator<T>,並且可以在新的 IAsyncEnumerable<T>中消耗 await foreachIAsyncDisposable 介面也可用來啟用異步清除。

詳細設計

介面

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 不需要具線程安全性,也不需要支持並行呼叫)。 此外,型別可以同時實作 IDisposableIAsyncDisposable,而且如果實作,同樣可以接受叫用 Dispose,然後 DisposeAsync 反之亦然,但只有第一個應該有意義,後續叫用兩者都應該是 nop。 因此,如果某類型確實實作了這兩者,建議使用者根據內容只呼叫一次更相關的方法:在同步上下文中呼叫 Dispose,在異步上下文中則呼叫 DisposeAsync

IAsyncDisposable 如何與 using 互動屬於單獨的討論。至於它與 foreach 的互動,本文會在稍後說明。)

考慮的替代方案:

  • DisposeAsync 接受 CancellationToken:理論上,任何異步操作都可以被取消,但處置涉及清理、關閉事物、釋放資源等,通常這些不應被取消;即使工作已被取消,清理仍然非常重要。 造成實際工作取消的相同 CancellationToken 通常會是傳遞至 DisposeAsync的相同令牌,因此 DisposeAsync 毫無價值,因為取消工作會導致 DisposeAsync 成為 no-op。 如果有人想要避免因等待處置而被封鎖,他們可以避免等待ValueTask的結果,或只等待它一段時間。
  • 傳回 :現在非泛型 存在,而且可以從 建構,從 傳回 ,可讓現有的物件重複使用為代表最終異步完成 的承諾,並在 異步完成的情況下儲存 配置。
  • ConfigureAwait設定 :雖然這類概念的公開方式對 usingforeach和其他語言建構可能存在問題,但從介面的觀點來看,實際上並沒有執行任何 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:允許列舉器支援多個取用者。 在某些情況下,列舉值對於支援多個並行取用者而言非常重要。 當 MoveNextAsyncCurrent 是分開的,實作就無法將它們的使用達到原子性,因此無法實現該目的。 相反地,此方法提供一個單一的 TryGetNext 方法,支援推動列舉器向前並取得下一個項目,因此枚舉器可以視需要啟用原子性。 不過,通過給每一個消費者從共用的列舉集合中分配一個專屬的列舉器,這類情境可能被啟用。 此外,我們不想強制每個枚舉器都支援併發使用,因為這將在大多數不需要這種功能的情況下增加顯著的額外負擔,這意味著介面的使用者通常無法以此方式依賴。
  • 主要:效能MoveNextAsync / Current 方式每個作業需要兩次介面呼叫,而 WaitForNextAsync/TryGetNext 的最佳情況是,大部分的迴圈同步完成,使得具有 TryGetNext的緊密的內部迴圈,每個作業只有一次介面呼叫。 在介面呼叫主宰計算的情況下,這可能會產生可測量的影響。

不過,也有非微不足道的缺點,包括在手動使用這些時,複雜度明顯增加,以及在使用它們時更容易引入 Bug。 雖然效能優勢在微基準測試中顯現,但我們並不認為它們會對絕大多數實際使用產生顯著影響。 如果證實他們符合,我們可以以高亮顯示的方式引進第二組介面。

已考慮捨棄的選項:

  • ValueTask<bool> WaitForNextAsync(); bool TryGetNext(out T result);out 參數不能是共變數。 這裡同樣受到了一個小影響(這是嘗試模式中普遍存在的問題),這可能會在運行時對參考類型結果造成寫屏障。

取消

支援取消的方法有數種:

  1. IAsyncEnumerable<T> / IAsyncEnumerator<T> 不受取消影響,CancellationToken 不會出現在任何地方。 取消是透過邏輯方式將 CancellationToken 嵌入到可列舉和/或迭代器中,以任何適當的方式達成,例如呼叫迭代器時,將 CancellationToken 當做參數傳遞至迭代器方法,並在迭代器主體中使用它,就像對任何其他參數所做的那樣。
  2. IAsyncEnumerator<T>.GetAsyncEnumerator(CancellationToken):您將 CancellationToken 傳遞給 GetAsyncEnumerator,後續的 MoveNextAsync 作業會在可能的情況下尊重它。
  3. IAsyncEnumerator<T>.MoveNextAsync(CancellationToken):您會將 CancellationToken 傳遞給每個個別 MoveNextAsync 呼叫。
  4. 1 && 2:您會將 CancellationToken嵌入至可列舉集合/枚舉器,並將 CancellationToken傳遞至 GetAsyncEnumerator
  5. 1 && 3:您將 CancellationToken嵌入至您的可列舉項或列舉器中,並將 CancellationToken傳遞至 MoveNextAsync

從純理論的觀點來看,(5)是最具堅實性的,因為(a)MoveNextAsync 接受 CancellationToken 允許對取消的內容進行最細緻的控制,而(b)CancellationToken 則是其他任何可以作為參數傳遞給反覆運算器的類型,或是內嵌在任意類型中的類型等。

不過,該方法有多個問題:

  • 如何將 CancellationToken 傳遞到 GetAsyncEnumerator 並進入迭代器的主體? 我們可以公開一個新的 iterator 關鍵詞,你可以利用此關鍵詞來存取傳遞至 CancellationTokenGetEnumerator,但 a)這涉及很多額外的操作架構,b)我們將其提升為主要功能,c)99% 情況似乎都是在調用迭代器和調用GetAsyncEnumerator上使用相同的代碼,這種情況下,可以將 CancellationToken 作為參數傳遞到該方法。
  • 傳遞至 CancellationTokenMoveNextAsync 如何進入到方法體中? 更糟的是,好像它從 iterator 本機對象公開,其值可能會在 await 之間變更,這表示任何向令牌註冊的程式代碼都必須在等候之前從它取消註冊,然後在之後重新註冊;不論編譯程式是在反覆運算器或開發人員手動實作,都需要在每個 MoveNextAsync 呼叫中執行這類註冊和取消註冊,也可能會相當昂貴。
  • 開發人員如何取消 foreach 迴圈? 如果藉由將 CancellationToken 提供給可列舉/列舉器來完成,那麼,我們要麼 a)需要支援在列舉器上進行 foreach操作,使其成為一等公民,並且您需要開始考慮圍繞列舉器建立的生態系統(例如 LINQ 方法),要麼 b)我們需要在可列舉中內嵌 CancellationToken,藉由在 WithCancellation 上的某個 IAsyncEnumerable<T> 擴充方法來儲存提供的令牌,然後在傳回結構的 GetAsyncEnumerator 被調用時將其傳遞給包裝的可列舉的 GetAsyncEnumerator(忽略該令牌)。 或者,您可以只使用您在 foreach 主體中的 CancellationToken
  • 如果/當查詢理解被支持時,提供給 CancellationTokenGetEnumeratorMoveNextAsync 將如何被傳遞到每個子句中? 最簡單的方式只需讓子句擷取它,此時傳遞至 GetAsyncEnumerator/MoveNextAsync 的任何令牌會被忽略。

本檔先前的版本建議使用 (1),但我們自那以後切換到 (4)。

(1) 的兩個主要問題:

  • 可取消的列舉項生產者必須實作一些樣板程式碼,並且只能利用編譯器對非同步迭代器的支援來實作 IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken) 方法。
  • 可能許多製造者會傾向於僅將 CancellationToken 參數新增到其異步可列舉物件的簽章中,這樣在獲得 IAsyncEnumerable 類型時,就能防止取用者傳遞其想要的取消令牌。

有兩個主要的消費情境:

  1. await foreach (var i in GetData(token)) ... 消費者呼叫 async-iterator 方法的位置,
  2. await foreach (var i in givenIAsyncEnumerable.WithCancellation(token)) ... 消費者處理指定 IAsyncEnumerable 實例。

我們發現,為了方便異步數據流的產生者和使用者,可以使用在異步迭代器方法中帶有註解的參數,以合理支援這兩種情境。 [EnumeratorCancellation] 屬性用於此用途。 將這個屬性放在參數上會告訴編譯程式,如果令牌傳遞至 GetAsyncEnumerator 方法,則應該使用該令牌,而不是原本為 參數傳遞的值。

請考慮 IAsyncEnumerable<int> GetData([EnumeratorCancellation] CancellationToken token = default)。 這個方法的實作者只要在方法主體中使用 參數即可。 消費者可以使用上述任一消費模式:

  1. 如果您使用 GetData(token),則令牌會儲存到異步列舉中,並將用於反覆運算中,
  2. 如果您使用 givenIAsyncEnumerable.WithCancellation(token),則傳遞至 GetAsyncEnumerator 的令牌將會取代儲存在異步列舉中的任何令牌。

foreach

除了現有對 foreach的支援之外,IAsyncEnumerable<T> 也會增強以支援 IEnumerable<T>。 如果相關成員是公開的,則支援與 IAsyncEnumerable<T> 相同的模式;如果未公開,則直接使用介面。如此便可以啟用基於結構的擴充功能,從而避免配置,並將替代的等候物用作 MoveNextAsyncDisposeAsync的回傳型別。

語法

使用語法:

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();
      }
    
  • 否則,如果存在從 ESystem.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 只能用於包含 asynciterator 方法;隨後,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>搭配使用的多載。 此外,大量的重載涉及處理述詞(例如採用 WhereFunc<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 的連結庫則提供各種緩衝處理數據結構。 此階段不需要涉及語言。