Jawaban:
Anda tidak dapat memiliki metode ref
atau out
parameter async .
Lucian Wischik menjelaskan mengapa ini tidak mungkin pada utas MSDN ini: http://social.msdn.microsoft.com/Forums/en-US/d2f48a52-e35a-4948-844d-828a1a6deb74/why-async-methods-cannot-have -ref-atau-out-parameter
Adapun mengapa metode async tidak mendukung parameter out-by-reference? (atau parameter ref?) Itu batasan dari CLR. Kami memilih untuk mengimplementasikan metode async dengan cara yang mirip dengan metode iterator - yaitu melalui kompiler yang mentransformasikan metode tersebut menjadi objek-mesin-objek. CLR tidak memiliki cara aman untuk menyimpan alamat "parameter keluar" atau "parameter referensi" sebagai bidang objek. Satu-satunya cara untuk mendukung parameter referensi-keluar adalah jika fitur async dilakukan oleh penulisan ulang CLR tingkat rendah alih-alih penulisan ulang kompiler. Kami memeriksa pendekatan itu, dan banyak yang harus dilakukan untuk itu, tetapi pada akhirnya akan sangat mahal sehingga tidak akan pernah terjadi.
Solusi khas untuk situasi ini adalah memiliki metode async mengembalikan Tuple sebagai gantinya. Anda dapat menulis ulang metode Anda seperti itu:
public async Task Method1()
{
var tuple = await GetDataTaskAsync();
int op = tuple.Item1;
int result = tuple.Item2;
}
public async Task<Tuple<int, int>> GetDataTaskAsync()
{
//...
return new Tuple<int, int>(1, 2);
}
Tuple
alternatifnya. Sangat membantu.
Tuple
. : P
Anda tidak dapat memiliki ref
atau out
parameter dalam async
metode (seperti yang telah dicatat).
Ini menjerit untuk beberapa pemodelan dalam data yang bergerak di sekitar:
public class Data
{
public int Op {get; set;}
public int Result {get; set;}
}
public async void Method1()
{
Data data = await GetDataTaskAsync();
// use data.Op and data.Result from here on
}
public async Task<Data> GetDataTaskAsync()
{
var returnValue = new Data();
// Fill up returnValue
return returnValue;
}
Anda mendapatkan kemampuan untuk menggunakan kembali kode Anda dengan lebih mudah, plus cara itu lebih mudah dibaca daripada variabel atau tupel.
Solusi C # 7 + adalah menggunakan sintaks tuple implisit.
private async Task<(bool IsSuccess, IActionResult Result)> TryLogin(OpenIdConnectRequest request)
{
return (true, BadRequest(new OpenIdErrorResponse
{
Error = OpenIdConnectConstants.Errors.AccessDenied,
ErrorDescription = "Access token provided is not valid."
}));
}
hasil kembali menggunakan metode tanda tangan nama properti yang ditentukan. misalnya:
var foo = await TryLogin(request);
if (foo.IsSuccess)
return foo.Result;
Alex membuat poin bagus pada keterbacaan. Sama dengan itu, suatu fungsi juga antarmuka yang cukup untuk menentukan jenis yang dikembalikan dan Anda juga mendapatkan nama variabel yang bermakna.
delegate void OpDelegate(int op);
Task<bool> GetDataTaskAsync(OpDelegate callback)
{
bool canGetData = true;
if (canGetData) callback(5);
return Task.FromResult(canGetData);
}
Penelepon menyediakan lambda (atau fungsi bernama) dan intellisense membantu dengan menyalin nama variabel dari delegasi.
int myOp;
bool result = await GetDataTaskAsync(op => myOp = op);
Pendekatan khusus ini seperti metode "Coba" di mana myOp
diatur jika hasil metode true
. Kalau tidak, Anda tidak peduli myOp
.
Salah satu fitur out
parameter yang bagus adalah mereka dapat digunakan untuk mengembalikan data bahkan ketika suatu fungsi melempar pengecualian. Saya pikir setara terdekat untuk melakukan ini dengan async
metode akan menggunakan objek baru untuk menyimpan data yang async
dapat merujuk pada metode dan pemanggil. Cara lain adalah dengan meloloskan seorang delegasi seperti yang disarankan dalam jawaban lain .
Perhatikan bahwa tidak satu pun dari teknik ini akan memiliki semacam penegakan dari kompiler yang out
dimiliki. Yaitu, kompiler tidak akan mengharuskan Anda untuk menetapkan nilai pada objek bersama atau memanggil delegasi yang lewat.
Berikut adalah contoh implementasi menggunakan objek bersama untuk meniru ref
dan out
untuk digunakan dengan async
metode dan berbagai skenario lainnya di mana ref
dan out
tidak tersedia:
class Ref<T>
{
// Field rather than a property to support passing to functions
// accepting `ref T` or `out T`.
public T Value;
}
async Task OperationExampleAsync(Ref<int> successfulLoopsRef)
{
var things = new[] { 0, 1, 2, };
var i = 0;
while (true)
{
// Fourth iteration will throw an exception, but we will still have
// communicated data back to the caller via successfulLoopsRef.
things[i] += i;
successfulLoopsRef.Value++;
i++;
}
}
async Task UsageExample()
{
var successCounterRef = new Ref<int>();
// Note that it does not make sense to access successCounterRef
// until OperationExampleAsync completes (either fails or succeeds)
// because there’s no synchronization. Here, I think of passing
// the variable as “temporarily giving ownership” of the referenced
// object to OperationExampleAsync. Deciding on conventions is up to
// you and belongs in documentation ^^.
try
{
await OperationExampleAsync(successCounterRef);
}
finally
{
Console.WriteLine($"Had {successCounterRef.Value} successful loops.");
}
}
Saya suka Try
polanya. Ini pola yang rapi.
if (double.TryParse(name, out var result))
{
// handle success
}
else
{
// handle error
}
Tapi, itu menantang async
. Itu tidak berarti kita tidak memiliki opsi nyata. Berikut adalah tiga pendekatan inti yang dapat Anda pertimbangkan untuk async
metode dalam versi kuasi dari Try
pola.
Ini terlihat paling seperti Try
metode sinkronisasi hanya mengembalikan tuple
bukan dengan bool
dengan out
parameter, yang kita semua tahu tidak diizinkan dalam C #.
var result = await DoAsync(name);
if (result.Success)
{
// handle success
}
else
{
// handle error
}
Dengan metode yang kembali true
dari false
dan tidak pernah melempar exception
.
Ingat, melemparkan pengecualian dalam suatu
Try
metode akan merusak seluruh tujuan pola.
async Task<(bool Success, StorageFile File, Exception exception)> DoAsync(string fileName)
{
try
{
var folder = ApplicationData.Current.LocalCacheFolder;
return (true, await folder.GetFileAsync(fileName), null);
}
catch (Exception exception)
{
return (false, null, exception);
}
}
Kita dapat menggunakan anonymous
metode untuk mengatur variabel eksternal. Sintaksnya pintar, meski sedikit rumit. Dalam dosis kecil, tidak apa-apa.
var file = default(StorageFile);
var exception = default(Exception);
if (await DoAsync(name, x => file = x, x => exception = x))
{
// handle success
}
else
{
// handle failure
}
Metode ini mematuhi dasar-dasar Try
pola tetapi menetapkan out
parameter untuk diteruskan dalam metode panggilan balik. Ini dilakukan seperti ini.
async Task<bool> DoAsync(string fileName, Action<StorageFile> file, Action<Exception> error)
{
try
{
var folder = ApplicationData.Current.LocalCacheFolder;
file?.Invoke(await folder.GetFileAsync(fileName));
return true;
}
catch (Exception exception)
{
error?.Invoke(exception);
return false;
}
}
Ada pertanyaan dalam benak saya tentang kinerja di sini. Tapi, kompiler C # sangat pintar, sehingga saya pikir Anda aman memilih opsi ini, hampir pasti.
Bagaimana jika Anda hanya menggunakan yang TPL
dirancang? Tidak ada tupel. Idenya di sini adalah bahwa kami menggunakan pengecualian untuk mengarahkan ulang ContinueWith
ke dua jalur yang berbeda.
await DoAsync(name).ContinueWith(task =>
{
if (task.Exception != null)
{
// handle fail
}
if (task.Result is StorageFile sf)
{
// handle success
}
});
Dengan metode yang melempar exception
ketika ada segala jenis kegagalan. Itu berbeda dari mengembalikan a boolean
. Ini cara untuk berkomunikasi dengan TPL
.
async Task<StorageFile> DoAsync(string fileName)
{
var folder = ApplicationData.Current.LocalCacheFolder;
return await folder.GetFileAsync(fileName);
}
Dalam kode di atas, jika file tidak ditemukan, pengecualian dilemparkan. Ini akan memanggil kegagalan ContinueWith
yang akan menangani Task.Exception
blok logikanya. Rapi, ya?
Dengar, ada alasan mengapa kita menyukai
Try
polanya. Ini pada dasarnya sangat rapi dan mudah dibaca dan, akibatnya, dapat dipertahankan. Ketika Anda memilih pendekatan Anda, anjing penjaga agar mudah dibaca. Ingat pengembang berikutnya yang dalam 6 bulan dan tidak memiliki Anda untuk menjawab pertanyaan klarifikasi. Kode Anda dapat menjadi satu-satunya dokumentasi yang pernah dimiliki pengembang.
Semoga berhasil.
ContinueWith
panggilan chaining memiliki hasil yang diharapkan? Menurut pemahaman saya yang kedua ContinueWith
akan memeriksa keberhasilan kelanjutan pertama, bukan keberhasilan tugas aslinya.
Saya memiliki masalah yang sama seperti saya suka menggunakan Try-method-pattern yang pada dasarnya tampaknya tidak sesuai dengan async-await-paradigm ...
Yang penting bagi saya adalah bahwa saya dapat memanggil Try-method dalam satu if-klausa dan tidak harus menentukan variabel-out sebelumnya, tetapi dapat melakukannya secara in-line seperti pada contoh berikut:
if (TryReceive(out string msg))
{
// use msg
}
Jadi saya datang dengan solusi berikut:
Tentukan struct pembantu:
public struct AsyncOut<T, OUT>
{
private readonly T returnValue;
private readonly OUT result;
public AsyncOut(T returnValue, OUT result)
{
this.returnValue = returnValue;
this.result = result;
}
public T Out(out OUT result)
{
result = this.result;
return returnValue;
}
public T ReturnValue => returnValue;
public static implicit operator AsyncOut<T, OUT>((T returnValue ,OUT result) tuple) =>
new AsyncOut<T, OUT>(tuple.returnValue, tuple.result);
}
Tetapkan async Try-method seperti ini:
public async Task<AsyncOut<bool, string>> TryReceiveAsync()
{
string message;
bool success;
// ...
return (success, message);
}
Panggil metode Try async seperti ini:
if ((await TryReceiveAsync()).Out(out string msg))
{
// use msg
}
Untuk beberapa parameter keluar, Anda dapat menentukan struct tambahan (mis. AsyncOut <T, OUT1, OUT2>) atau Anda dapat mengembalikan tuple.
Batasan async
metode yang tidak menerima out
parameter hanya berlaku untuk metode async yang dihasilkan oleh kompiler, ini dinyatakan dengan async
kata kunci. Itu tidak berlaku untuk metode async kerajinan tangan. Dengan kata lain dimungkinkan untuk membuat Task
metode pengembalian menerima out
parameter. Misalnya katakanlah kita sudah memiliki ParseIntAsync
metode yang melempar, dan kami ingin membuat TryParseIntAsync
yang tidak melempar. Kita bisa menerapkannya seperti ini:
public static Task<bool> TryParseIntAsync(string s, out Task<int> result)
{
var tcs = new TaskCompletionSource<int>();
result = tcs.Task;
return ParseIntAsync(s).ContinueWith(t =>
{
if (t.IsFaulted)
{
tcs.SetException(t.Exception.InnerException);
return false;
}
tcs.SetResult(t.Result);
return true;
}, default, TaskContinuationOptions.None, TaskScheduler.Default);
}
Menggunakan TaskCompletionSource
dan ContinueWith
metode ini agak canggung, tetapi tidak ada pilihan lain karena kita tidak dapat menggunakan await
kata kunci yang mudah digunakan di dalam metode ini.
Contoh penggunaan:
if (await TryParseIntAsync("-13", out var result))
{
Console.WriteLine($"Result: {await result}");
}
else
{
Console.WriteLine($"Parse failed");
}
Pembaruan: Jika logika async terlalu kompleks untuk diekspresikan tanpa await
, maka itu bisa dienkapsulasi di dalam delegasi anonim sinkron asinkron. A TaskCompletionSource
masih diperlukan untuk out
parameter. Ada kemungkinan bahwa out
parameter dapat diselesaikan sebelum penyelesaian tugas utama, seperti dalam contoh di bawah ini:
public static Task<string> GetDataAsync(string url, out Task<int> rawDataLength)
{
var tcs = new TaskCompletionSource<int>();
rawDataLength = tcs.Task;
return ((Func<Task<string>>)(async () =>
{
var response = await GetResponseAsync(url);
var rawData = await GetRawDataAsync(response);
tcs.SetResult(rawData.Length);
return await FilterDataAsync(rawData);
}))();
}
Contoh ini mengasumsikan adanya tiga metode asinkron GetResponseAsync
, GetRawDataAsync
dan FilterDataAsync
itu disebut berturut-turut. The out
parameter selesai pada penyelesaian metode kedua. The GetDataAsync
Metode dapat digunakan seperti ini:
var data = await GetDataAsync("http://example.com", out var rawDataLength);
Console.WriteLine($"Data: {data}");
Console.WriteLine($"RawDataLength: {await rawDataLength}");
Menunggu data
sebelum menunggu rawDataLength
adalah penting dalam contoh sederhana ini, karena dalam kasus pengecualian out
parameter tidak akan pernah selesai.
Saya pikir menggunakan ValueTuples seperti ini bisa berhasil. Anda harus menambahkan paket ValueTuple NuGet terlebih dahulu:
public async void Method1()
{
(int op, int result) tuple = await GetDataTaskAsync();
int op = tuple.op;
int result = tuple.result;
}
public async Task<(int op, int result)> GetDataTaskAsync()
{
int x = 5;
int y = 10;
return (op: x, result: y):
}
Berikut kode jawaban @ dcastro yang dimodifikasi untuk C # 7.0 dengan nama tuple dan tuple deconstruction, yang merampingkan notasi:
public async void Method1()
{
// Version 1, named tuples:
// just to show how it works
/*
var tuple = await GetDataTaskAsync();
int op = tuple.paramOp;
int result = tuple.paramResult;
*/
// Version 2, tuple deconstruction:
// much shorter, most elegant
(int op, int result) = await GetDataTaskAsync();
}
public async Task<(int paramOp, int paramResult)> GetDataTaskAsync()
{
//...
return (1, 2);
}
Untuk detail tentang tupel bernama baru, tuple literal dan dekonstruksi tuple lihat: https://blogs.msdn.microsoft.com/dotnet/2017/03/09/new-features-in-c-7-0/
Anda dapat melakukan ini dengan menggunakan TPL (task parallel library) alih-alih langsung menggunakan kata kunci tunggu.
private bool CheckInCategory(int? id, out Category category)
{
if (id == null || id == 0)
category = null;
else
category = Task.Run(async () => await _context.Categories.FindAsync(id ?? 0)).Result;
return category != null;
}
if(!CheckInCategory(int? id, out var category)) return error