فرض کنید قصد دارید یک متد async از نوع IEnumerable را که تعدادی yield return به تاخیر افتاده را به همراه دارد (yield returnها فقط زمانی اجرا میشوند که بر روی آنها متدهایی مانند ToList و یا حلقهی foreach اجرا شوند) و همچنین توسط await Task.Delay، دریافت اطلاعات به صورت async را نیز شبیه سازی میکند، تهیه کنید:
public struct Statement { public int Id { get; } public string Description { get; } public Statement(int id, string description) => (Id, Description) = (id, description); public override string ToString() => Description; } public static async Task<IEnumerable<Statement>> GetStatements(bool error) { if (error) { throw new Exception("Oops, we messed up 😬"); } await Task.Delay(1000); //Simulate waiting for data to come through. yield return new Statement(1, "C# is cool!"); yield return new Statement(2, "C# orginally named COOL."); yield return new Statement(3, "More examples..."); }
The body of 'AsyncStreams.GetStatements(bool)' cannot be an iterator block because 'Task<IEnumerable<AsyncStreams.Statement>>' is not an iterator interface type (CS1624)
برای رفع یک چنین مشکلی، اکنون در C# 8.0 میتوان از اینترفیس جدید IAsyncEnumerable بجای Task IEnumerable استفاده کرد. به این ترتیب تنها تغییری که در قطعه کد فوق نیاز است، تغییر امضای آن به صورت زیر است:
static async IAsyncEnumerable<Statement> GetStatements(bool error)
امکان تعریف حلقههای async در C# 8.0
مرحلهی بعد، ایجاد حلقهای بر روی متد GetStatements است. اکنون مشکل دیگری وجود دارد: حلقهی foreach به خودی خود، یک حلقهی synchronous است و اگر از آن برای کار با یک استریم async استفاده شود، هربار که اطلاعاتی از آن بازگشت داده میشود، پایان یک Task نیز گزارش داده خواهد شد که میتوان سبب خاتمهی حلقه شود. بنابراین انجام اینکار نیز پیش از C# 8.0 میسر نبود که اکنون با امکان تعریف await پیش از یک حلقهی foreach، ممکن شدهاست:
static async IAsyncEnumerable<Statement> GetStatementsAsync(bool error) { await foreach (var statement in GetStatements(error)) { await Task.Delay(1000); yield return statement; } }
اینترفیس IAsyncEnumerable چگونه تعریف شدهاست؟
اینترفیس IAsyncEnumerable متد GetAsyncEnumerator را تعریف میکند که یک IAsyncEnumerator را بازگشت میدهد و آن نیز به همراه متد MoveNextAsync است. اگر دقت کنید در این حالت از نگارش async اینترفیس IDisposable به نام IAsyncDisposable استفاده کردهاست:
using System.Threading; namespace System.Collections.Generic { public interface IAsyncEnumerable<out T> { IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default); } public interface IAsyncEnumerator<out T> : IAsyncDisposable { T Current { get; } ValueTask<bool> MoveNextAsync(); } } namespace System { public interface IAsyncDisposable { ValueTask DisposeAsync(); } }
مثالی از IAsyncDisposable و روش Dispose خودکار آن
با معرفی IAsyncDisposable، اگر یک مثال ساده از پیاده سازی آن به صورت زیر باشد:
public class AwaitUsingTest : IAsyncDisposable { public async ValueTask DisposeAsync() { await Task.CompletedTask; } public void Dummy() { } }
async Task FooBar() { await using var test = new AwaitUsingTest(); test.Dummy(); }