اندازهی قلم متن
تخمین مدت زمان مطالعهی مطلب:
سه دقیقه
امکان تعریف نوعهای شمارشی async در C# 8.0
فرض کنید قصد دارید یک متد async از نوع IEnumerable را که تعدادی yield return به تاخیر افتاده را به همراه دارد (yield returnها فقط زمانی اجرا میشوند که بر روی آنها متدهایی مانند ToList و یا حلقهی foreach اجرا شوند) و همچنین توسط await Task.Delay، دریافت اطلاعات به صورت async را نیز شبیه سازی میکند، تهیه کنید:
این قطعه کد حتی در C# 8.0 نیز چنین خطای کامپایلری را به همراه دارد:
عنوان میکند که برای دریافت اطلاعات متد GetStatements باید یک iterator تشکیل شود؛ اما Task IEnumerable از این نوع نیست.
برای رفع یک چنین مشکلی، اکنون در C# 8.0 میتوان از اینترفیس جدید IAsyncEnumerable بجای Task IEnumerable استفاده کرد. به این ترتیب تنها تغییری که در قطعه کد فوق نیاز است، تغییر امضای آن به صورت زیر است:
امکان تعریف حلقههای async در C# 8.0
مرحلهی بعد، ایجاد حلقهای بر روی متد GetStatements است. اکنون مشکل دیگری وجود دارد: حلقهی foreach به خودی خود، یک حلقهی synchronous است و اگر از آن برای کار با یک استریم async استفاده شود، هربار که اطلاعاتی از آن بازگشت داده میشود، پایان یک Task نیز گزارش داده خواهد شد که میتوان سبب خاتمهی حلقه شود. بنابراین انجام اینکار نیز پیش از C# 8.0 میسر نبود که اکنون با امکان تعریف await پیش از یک حلقهی foreach، ممکن شدهاست:
تا پیش از C# 8.0، از واژهی await تنها برای دریافت یک تک مقدار استفاده میشد؛ اما حالا میتوان از آن برای دریافت استریمی از نتایج (async streams) نیز استفاده کرد.
اینترفیس IAsyncEnumerable چگونه تعریف شدهاست؟
اینترفیس IAsyncEnumerable متد GetAsyncEnumerator را تعریف میکند که یک IAsyncEnumerator را بازگشت میدهد و آن نیز به همراه متد MoveNextAsync است. اگر دقت کنید در این حالت از نگارش async اینترفیس IDisposable به نام IAsyncDisposable استفاده کردهاست:
اینترفیسهای IAsyncDisposable و IAsyncEnumerator یک ValueTask را توسط متدهای DisposeAsync و MoveNextAsync بازگشت میدهند و این مورد به C# 7x باز میگردد که امکان await را نه تنها بر روی Task، بلکه بر روی هر نوعی که متد GetAwaiter را پیاده سازی میکند، میسر میکند و ValueTask نیز یکی از آنها است. ValueTask به صورت یک نوع مقدار (value type) تعریف شدهاست؛ بجای نوع ارجاعی Task که سربار کمتری را به همراه دارد.
مثالی از IAsyncDisposable و روش Dispose خودکار آن
با معرفی IAsyncDisposable، اگر یک مثال ساده از پیاده سازی آن به صورت زیر باشد:
روش فراخوانی using declaration بر روی آن به همراه واژهی کلیدی await در C# 8.0، مانند مثال زیر است:
فرض کنید قصد دارید یک متد 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(); }