
Jenis pengembalian async yang digeneralisasi - ini adalah fitur C # 7 baru yang memungkinkan tidak hanya menggunakan Tugas sebagai jenis pengembalian metode async tetapi juga jenis lain (kelas atau struktur) yang memenuhi beberapa persyaratan khusus.
Pada saat yang sama, async / menunggu adalah cara untuk memanggil serangkaian fungsi "kelanjutan" di dalam beberapa konteks yang merupakan inti dari pola desain lain - Monad . Jadi, bisakah kita menggunakan async / menunggu untuk menulis kode yang akan berperilaku sama seperti jika kita menggunakan monads? Ternyata - ya (dengan beberapa pemesanan). Misalnya, kode di bawah ini dapat dikompilasi dan berfungsi:
async Task Main() { foreach (var s in new[] { "1,2", "3,7,1", null, "1" }) { var res = await Sum(s).GetMaybeResult(); Console.WriteLine(res.IsNothing ? "Nothing" : res.GetValue().ToString()); }
Selanjutnya, saya akan menjelaskan bagaimana kodenya bekerja ...
Jenis pengembalian async umum
Pertama-tama mari kita cari tahu apa yang diperlukan untuk menggunakan tipe kita sendiri (mis. MyAwaitable <T> ) sebagai tipe hasil dari beberapa fungsi async. Dokumentasi mengatakan bahwa jenis tersebut harus memiliki:
Metode GetAwaiter () yang mengembalikan objek tipe yang mengimplementasikan antarmuka INotifyCompletion dan memiliki bool properti IsCompleted dan metode T GetResult () ;
Atribut [AsyncMethodBuilder (Type)] yang menunjuk ke kelas "method builder" ( atau struktur ) misalnya MyAwaitableTaskMethodBuilder <T> dengan metode berikut:
- statis Buat ()
- Mulai (stateMachine)
- SetResult (hasil)
- SetException (pengecualian)
- SetStateMachine (stateMachine)
- AwaitOnCompleted (menunggu, stateMachine)
- AwaitUnsafeOnCompleted (menunggu, stateMachine)
- Tugas
Berikut ini adalah implementasi sederhana dari MyAwaitable dan MyAwaitableTaskMethodBuilder [AsyncMethodBuilder(typeof(MyAwaitableTaskMethodBuilder<>))] public class MyAwaitable<T> : INotifyCompletion { private Action _continuation; public MyAwaitable() { } public MyAwaitable(T value) { this.Value = value; this.IsCompleted = true; } public MyAwaitable<T> GetAwaiter() => this; public bool IsCompleted { get; private set; } public T Value { get; private set; } public Exception Exception { get; private set; } public T GetResult() { if (!this.IsCompleted) throw new Exception("Not completed"); if (this.Exception != null) { ExceptionDispatchInfo.Throw(this.Exception); } return this.Value; } internal void SetResult(T value) { if (this.IsCompleted) throw new Exception("Already completed"); this.Value = value; this.IsCompleted = true; this._continuation?.Invoke(); } internal void SetException(Exception exception) { this.IsCompleted = true; this.Exception = exception; } void INotifyCompletion.OnCompleted(Action continuation) { this._continuation = continuation; if (this.IsCompleted) { continuation(); } } } public class MyAwaitableTaskMethodBuilder<T> { public MyAwaitableTaskMethodBuilder() => this.Task = new MyAwaitable<T>(); public static MyAwaitableTaskMethodBuilder<T> Create() => new MyAwaitableTaskMethodBuilder<T>(); public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine => stateMachine.MoveNext(); public void SetStateMachine(IAsyncStateMachine stateMachine) { } public void SetException(Exception exception) => this.Task.SetException(exception); public void SetResult(T result) => this.Task.SetResult(result); public void AwaitOnCompleted<TAwaiter, TStateMachine>( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine => this.GenericAwaitOnCompleted(ref awaiter, ref stateMachine); public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine => this.GenericAwaitOnCompleted(ref awaiter, ref stateMachine); public void GenericAwaitOnCompleted<TAwaiter, TStateMachine>( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine => awaiter.OnCompleted(stateMachine.MoveNext); public MyAwaitable<T> Task { get; } }
Sekarang kita dapat menggunakan MyAwaitable sebagai jenis hasil dari metode async:
private async MyAwaitable<int> MyAwaitableMethod() { int result = 0; int arg1 = await this.GetMyAwaitable(1); result += arg1; int arg2 = await this.GetMyAwaitable(2); result += arg2; int arg3 = await this.GetMyAwaitable(3); result += arg3; return result; } private async MyAwaitable<int> GetMyAwaitable(int arg) { await Task.Delay(1);
Kode berfungsi seperti yang diharapkan tetapi untuk memahami tujuan persyaratan untuk MyAwaitable, mari kita lihat apa yang dilakukan preprocessor C # dengan MyAwaitableMethod . Jika Anda menjalankan util de-kompilasi (mis. DotPeek), Anda akan melihat bahwa metode asli diubah sebagai berikut:
private MyAwaitable<int> MyAwaitableMethod() { var stateMachine = new MyAwaitableMethodStateMachine(); stateMachine.Owner = this; stateMachine.Builder = MyAwaitableTaskMethodBuilder<int>.Create(); stateMachine.State = 0; stateMachine.Builder.Start(ref stateMachine); return stateMachine.Builder.Task; }
MyAwaitableMethodStateMachineSebenarnya, ini adalah kode yang disederhanakan di mana saya menghilangkan banyak optimasi untuk membuat kode yang dihasilkan kompiler dapat dibaca
sealed class MyAwaitableMethodStateMachine : IAsyncStateMachine { public int State; public MyAwaitableTaskMethodBuilder<int> Builder; public BuilderDemo Owner; private int _result; private int _arg1; private int _arg2; private int _arg3; private MyAwaitableAwaiter<int> _awaiter1; private MyAwaitableAwaiter<int> _awaiter2; private MyAwaitableAwaiter<int> _awaiter3; private void SetAwaitCompletion(INotifyCompletion awaiter) { var stateMachine = this; this.Builder.AwaitOnCompleted(ref awaiter, ref stateMachine); } void IAsyncStateMachine.MoveNext() { int finalResult; try { label_begin: switch (this.State) { case 0: this._result = 0; this._awaiter1 = this.Owner.GetMyAwaitable(1).GetAwaiter(); this.State = 1; if (!this._awaiter1.IsCompleted) { this.SetAwaitCompletion(this._awaiter1); return; } goto label_begin; case 1:
Meninjau kode yang dihasilkan kita dapat melihat bahwa "pembuat metode" memiliki tanggung jawab berikut:
- Penjadwalan mesin negara MoveNext () memanggil metode ketika operasi asinkron anak dilakukan (dalam skenario paling sederhana kita hanya meneruskan MoveNext () ke OnCompleted () dari operasi async yang menunggu).
- Pembuatan objek konteks operasi asinkron (
public MyAwaitable<T> Task { get; }
) - Bereaksi pada keadaan akhir dari mesin negara yang dihasilkan: SetResult atau SetException .
Dengan kata lain, dengan "pembangun metode" kita bisa mendapatkan kontrol tentang bagaimana metode asinkron dijalankan dan kelihatannya seperti fitur yang akan membantu kita mencapai tujuan kita - sebuah implementasi dari perilaku Mungkin monad. Tapi apa gunanya monad itu? Baiklah ... Anda dapat menemukan banyak artikel tentang monad itu di Internet, jadi di sini saya akan menjelaskan dasar-dasarnya saja.
Mungkin monad
Singkatnya, Mungkin monad adalah pola desain yang memungkinkan gangguan rantai panggilan fungsi jika beberapa fungsi dari rantai tidak dapat menghasilkan hasil yang berharga (misalnya kesalahan parsing).
Secara historis, bahasa pemrograman imperatif telah memecahkan masalah dengan dua cara:
- Banyak logika kondisional
- Pengecualian
Kedua cara memiliki kerugian yang jelas, sehingga cara ketiga ditemukan:
- Buat jenis yang dapat di 2 negara: "Beberapa Nilai" dan "Tidak Ada" - sebut saja "Mungkin"
- Buat fungsi (sebut saja "SelectMany") yang mengambil 2 argumen:
2.1. Objek dengan tipe "Maybe"
2.2. Fungsi berikutnya dari set panggilan - fungsi juga harus mengembalikan objek "Mungkin" yang akan berisi hasil atau "Tidak Ada" jika hasilnya tidak dapat dievaluasi (mis. Parameter fungsi tidak dalam format yang benar) - Fungsi "SelectMany" memeriksa apakah "Mungkin" memiliki beberapa nilai dan kemudian memanggil fungsi berikutnya menggunakan nilai (diekstrak dari "Mungkin") sebagai argumen dan kemudian mengembalikan hasilnya, jika tidak mengembalikan objek "Mungkin" dalam keadaan "Tidak Ada" .

Di C # itu bisa diimplementasikan seperti itu:
public struct Maybe<T> { public static implicit operator Maybe<T>(T value) => Value(value); public static Maybe<T> Value(T value) => new Maybe<T>(false, value); public static readonly Maybe<T> Nothing = new Maybe<T>(true, default); private Maybe(bool isNothing, T value) { this.IsNothing = isNothing; this._value = value; } public readonly bool IsNothing; private readonly T _value; public T GetValue() => this.IsNothing ? throw new Exception("Nothing") : this._value; } public static class MaybeExtensions { public static Maybe<TRes> SelectMany<TIn, TRes>( this Maybe<TIn> source, Func<TIn, Maybe<TRes>> func) => source.IsNothing ? Maybe<TRes>.Nothing : func(source.GetValue()); }
dan penggunaan:
static void Main() { for (int i = 0; i < 10; i++) { var res = Function1(i).SelectMany(Function2).SelectMany(Function3); Console.WriteLine(res.IsNothing ? "Nothing" : res.GetValue().ToString()); } Maybe<int> Function1(int acc) => acc < 10 ? acc + 1 : Maybe<int>.Nothing; Maybe<int> Function2(int acc) => acc < 10 ? acc + 2 : Maybe<int>.Nothing; Maybe<int> Function3(int acc) => acc < 10 ? acc + 3 : Maybe<int>.Nothing; }
Mengapa 'SelectMany'?Saya pikir beberapa dari Anda mungkin mengajukan pertanyaan: "Mengapa penulis memanggil fungsi" SelectMany "? Itu nama yang sangat aneh". Memang, tapi ada alasan untuk itu. Dalam C # SelectMany digunakan oleh preprocessor untuk mendukung notasi kueri yang menyederhanakan bekerja dengan rantai panggilan (Anda dapat menemukan rincian lebih lanjut di artikel saya sebelumnya )).
Bahkan, kita dapat mengubah rantai panggilan sebagai berikut:
var res = Function1(i) .SelectMany(x2 => Function2(x2).SelectMany(x3 => Function3(x3.SelectMany<int, int>(x4 => x2 + x3 + x4)));
sehingga kita bisa mendapatkan akses ke semua hasil antara yang nyaman tetapi kode ini sulit dibaca.
Di sini notasi kueri membantu kami:
var res = from x2 in Function1(i) from x3 in Function2(x2) from x4 in Function3(x3) select x2 + x3 + x4;
Untuk membuat kode dapat dikompilasi, kita memerlukan versi "Select Many" yang disempurnakan
public static Maybe<TJ> SelectMany<TIn, TRes, TJ>( this Maybe<TIn> source, Func<TIn, Maybe<TRes>> func, Func<TIn, TRes, TJ> joinFunc) { if (source.IsNothing) return Maybe<TJ>.Nothing; var res = func(source.GetValue()); return res.IsNothing ? Maybe<TJ>.Nothing : joinFunc(source.GetValue(), res.GetValue()); }
Mari kita implementasikan program dari header artikel menggunakan implementasi 'klasik' Mungkin 'ini static void Main() { foreach (var s in new[] {"1,2", "3,7,1", null, "1"}) { var res = Sum(s); Console.WriteLine(res.IsNothing ? "Nothing" : res.GetValue().ToString()); } Console.ReadKey(); } static Maybe<int> Sum(string input) => Split(input).SelectMany(items => Acc(0, 0, items));
Kode tidak terlihat bagus karena C # tidak dirancang sebagai bahasa fungsional, tetapi dalam bahasa fungsional "benar" seperti Haskell pendekatan seperti itu sangat umum
Async mungkin
Inti dari Mungkin monad adalah untuk mengontrol rantai panggilan fungsi, tetapi justru itulah "async / menunggu". Jadi mari kita coba menggabungkan keduanya. Pertama, kita perlu membuat Mungkin ketik kompatibel dengan fungsi asinkron dan kita sudah tahu bagaimana melakukannya:
[AsyncMethodBuilder(typeof(MaybeTaskMethodBuilder<>))] public class Maybe<T> : INotifyCompletion { ... public Maybe<T> GetAwaiter() => this; public bool IsCompleted { get; private set; } public void OnCompleted(Action continuation){...} public T GetResult() =>... }
Sekarang mari kita lihat bagaimana "klasik Mungkin" dapat ditulis ulang sebagai mesin negara untuk dapat menemukan kesamaan:
static void Main() { for (int i = 0; i < 10; i++) { var stateMachine = new StateMachine(); stateMachine.state = 0; stateMachine.i = i; stateMachine.MoveNext(); var res = stateMachine.Result; Console.WriteLine(res.IsNothing ? "Nothing" : res.GetValue().ToString()); } Console.ReadKey(); } class StateMachine { public int state = 0; public int i; public Maybe<int> Result; private Maybe<int> _f1; private Maybe<int> _f2; private Maybe<int> _f3; public void MoveNext() { label_begin: switch (this.state) { case 0: this._f1 = Function1(this.i); this.state = Match ? -1 : 1; goto label_begin; case 1: this._f2 = Function2(this._f1.GetValue()); this.state = this._f2.IsNothing ? -1 : 2; goto label_begin; case 2: this._f3 = Function3(this._f2.GetValue()); this.state = this._f3.IsNothing ? -1 : 3; goto label_begin; case 3: this.Result = this._f3.GetValue(); break; case -1: this.Result = Maybe<int>.Nothing; break; } } }
Jika kita mencocokkan mesin negara ini dengan yang dihasilkan oleh preprosesor C # (lihat di atas - 'MyAwaitableMethodStateMachine'), kita dapat melihat bahwa Mungkin pengecekan status dapat diterapkan di dalam:
this.Builder.AwaitOnCompleted(ref awaiter, ref stateMachine);
dimana ref awaiter
adalah objek dari Maybe type. Satu-satunya masalah di sini adalah bahwa kita tidak dapat mengatur mesin ke status "final" (-1). Apakah itu berarti bahwa kita tidak dapat mengontrol alur eksekusi? Sebenarnya tidak. Masalahnya adalah bahwa untuk setiap tindakan asinkron C # menetapkan tindakan panggilan balik melalui antarmuka INotifyCompletion , jadi jika kita ingin memecah aliran eksekusi kita bisa memanggil tindakan panggilan balik dalam kasus ketika kita tidak dapat melanjutkan aliran.
Tantangan lain di sini adalah bahwa mesin keadaan yang dihasilkan melewati tindakan berikutnya (sebagai panggilan balik kelanjutan) dari aliran saat ini, tetapi kita membutuhkan panggilan balik lanjutan dari aliran awal yang akan memungkinkan mem-bypass sisa operasi async:

Jadi, kita perlu menghubungkan tindakan async anak dengan leluhurnya. Kita dapat melakukannya dengan menggunakan "pembuat metode" yang memiliki tautan ke operasi async saat ini - Tugas . Tautan ke semua operasi async anak akan diteruskan ke AwaitOnCompleted(ref awaiter
as awaiter , jadi kita hanya perlu memeriksa apakah parameternya adalah instance dari Maybe dan jika kemudian mengatur current current sebagai induk untuk anak tersebut:
[AsyncMethodBuilder(typeof(MaybeTaskMethodBuilder<>))] public class Maybe<T> : IMaybe, INotifyCompletion { private IMaybe _parent; void IMaybe.SetParent(IMaybe parent) => this._parent = parent; ... } public class MaybeTaskMethodBuilder<T> { ... private void GenericAwaitOnCompleted<TAwaiter, TStateMachine>( ref TAwaiter awaiter, ref TStateMachine stateMachine) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine { if (awaiter is IMaybe maybe) { maybe.SetParent(this.Task); } awaiter.OnCompleted(stateMachine.MoveNext); } ... }
Sekarang semua objek Maybe dapat digabungkan menjadi sebuah pohon dan dan sebagai hasilnya, kita akan mendapatkan akses ke kelanjutan dari root Maybe ( Exit method) dari sembarang turunan:
[AsyncMethodBuilder(typeof(MaybeTaskMethodBuilder<>))] public class Maybe<T> : IMaybe, INotifyCompletion { private Action _continuation; private IMaybe _parent; ... public void OnCompleted(Action continuation) { ... this._continuation = continuation; ... } ... void IMaybe.Exit() { this.IsCompleted = true; if (this._parent != null) { this._parent.Exit(); } else { this._continuation(); } } ... }
Metode Exit harus dipanggil ketika (saat bergerak di atas pohon) kami menemukan objek Mungkin sudah diselesaikan dalam keadaan Tidak ada . Objek Maybe tersebut dapat dikembalikan dengan metode seperti ini:
Maybe<int> Parse(string str) => int.TryParse(str, out var result) ? result : Maybe<int>.Nothing();
Untuk menyimpan keadaan terselesaikan Mungkin mari kita perkenalkan struktur baru yang terpisah:
public struct MaybeResult { ... private readonly T _value; public readonly bool IsNothing; public T GetValue() => this.IsNothing ? throw new Exception("Nothing") : this._value; } [AsyncMethodBuilder(typeof(MaybeTaskMethodBuilder<>))] public class Maybe<T> : IMaybe, INotifyCompletion { private MaybeResult? _result; ... internal Maybe() { }
Ketika mesin negara async memanggil (melalui pembangun metode) Metode OnCompleted dari instance Mungkin sudah diselesaikan dan itu dalam keadaan Tidak ada kita akan dapat memecah seluruh aliran:
public void OnCompleted(Action continuation) { this._continuation = continuation; if(this._result.HasValue) { this.NotifyResult(this._result.Value.IsNothing); } } internal void SetResult(T result)
Sekarang satu-satunya yang tersisa - bagaimana mendapatkan hasil dari async Mungkin di luar ruang lingkupnya (metode async yang jenis pengembaliannya tidak Mungkin ). Jika Anda mencoba menggunakan kata kunci tunggu saja dengan contoh Mungkin maka pengecualian akan dilemparkan karena kode ini:
[AsyncMethodBuilder(typeof(MaybeTaskMethodBuilder<>))] public class Maybe<T> : IMaybe, INotifyCompletion { private MaybeResult? _result; public T GetResult() => this._result.Value.GetValue(); } ... public struct MaybeResult { ... public T GetValue() => this.IsNothing ? throw new Exception("Nothing") : this._value; }
Untuk mengatasi masalah ini, kita cukup menambahkan Awaiter baru yang akan mengembalikan seluruh struktur MaybeResult dan kita dapat menulis kode seperti ini:
var res = await GetResult().GetMaybeResult(); if(res.IsNothing){ ... } else{ res.GetValue(); ... };
Itu saja untuk saat ini. Dalam contoh kode saya menghilangkan beberapa detail untuk fokus hanya pada bagian yang paling penting. Anda dapat menemukan versi lengkapnya di github .
Namun , saya tidak akan merekomendasikan menggunakan versi ini dalam kode produksi apa pun karena memiliki masalah yang signifikan - ketika kita mengerem aliran eksekusi dengan memanggil kelanjutan dari root Mungkin kita akan melewati SEMUA! termasuk semua blok akhirnya (itu adalah jawaban untuk pertanyaan "Apakah akhirnya blok selalu dipanggil?"), sehingga semua operator yang menggunakan tidak akan bekerja seperti yang diharapkan dan itu mungkin menyebabkan kebocoran sumber daya. Masalah ini dapat diselesaikan jika alih-alih memanggil panggilan lanjutan lanjutan, kami akan memberikan pengecualian khusus yang akan ditangani secara internal (di sini Anda dapat menemukan versinya ), tetapi solusi ini tampaknya memiliki imitasi kinerja (yang mungkin dapat diterima dalam beberapa skenario). Dengan versi saat ini dari kompiler C # saya tidak melihat solusi lain tetapi itu mungkin berubah di masa depan.
Batasan ini tidak berarti bahwa semua trik yang dijelaskan dalam artikel ini sama sekali tidak berguna, mereka dapat digunakan untuk mengimplementasikan monad lain yang tidak memerlukan perubahan dalam alur eksekusi, misalnya "Pembaca". Bagaimana cara mengimplementasikan "Reader" itu melalui async / tunggu, saya akan tunjukkan di artikel selanjutnya .