Bagaimana cara menulis metode async tanpa parameter?

176

Saya ingin menulis metode async dengan outparameter, seperti ini:

public async void Method1()
{
    int op;
    int result = await GetDataTaskAsync(out op);
}

Bagaimana saya melakukan ini GetDataTaskAsync?

jesse
sumber

Jawaban:

279

Anda tidak dapat memiliki metode refatau outparameter 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);
}
dcastro
sumber
10
Jauh dari terlalu rumit, ini bisa menghasilkan terlalu banyak masalah. Jon Skeet menjelaskannya dengan sangat baik di sini stackoverflow.com/questions/20868103/…
MuiBienCarlota
3
Terima kasih atas Tuplealternatifnya. Sangat membantu.
Luke Vo
19
itu jelek Tuple. : P
tofutim
36
Saya pikir Named Tuples di C # 7 akan menjadi solusi sempurna untuk ini.
orad
3
@orad Saya terutama menyukai ini: private async Task <(keberhasilan bool, pekerjaan, pesan string)> TryGetJobAsync (...)
J. Andrew Laughlin
51

Anda tidak dapat memiliki refatau outparameter dalam asyncmetode (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.

Alex
sumber
2
Saya lebih suka solusi ini daripada menggunakan Tuple. Lebih bersih!
MiBol
31

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;
jv_
sumber
12

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 myOpdiatur jika hasil metode true. Kalau tidak, Anda tidak peduli myOp.

Scott Turner
sumber
9

Salah satu fitur outparameter yang bagus adalah mereka dapat digunakan untuk mengembalikan data bahkan ketika suatu fungsi melempar pengecualian. Saya pikir setara terdekat untuk melakukan ini dengan asyncmetode akan menggunakan objek baru untuk menyimpan data yang asyncdapat 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 outdimiliki. 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 refdan outuntuk digunakan dengan asyncmetode dan berbagai skenario lainnya di mana refdan outtidak 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.");
    }
}
binki
sumber
6

Saya suka Trypolanya. 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 asyncmetode dalam versi kuasi dari Trypola.

Pendekatan 1 - menampilkan struktur

Ini terlihat paling seperti Trymetode sinkronisasi hanya mengembalikan tuplebukan dengan booldengan outparameter, 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 truedari falsedan tidak pernah melempar exception.

Ingat, melemparkan pengecualian dalam suatu Trymetode 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);
    }
}

Pendekatan 2 - metode panggilan balik masuk

Kita dapat menggunakan anonymousmetode 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 Trypola tetapi menetapkan outparameter 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.

Pendekatan 3 - gunakan ContinueWith

Bagaimana jika Anda hanya menggunakan yang TPLdirancang? Tidak ada tupel. Idenya di sini adalah bahwa kami menggunakan pengecualian untuk mengarahkan ulang ContinueWithke 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 exceptionketika 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 ContinueWithyang akan menangani Task.Exceptionblok logikanya. Rapi, ya?

Dengar, ada alasan mengapa kita menyukai Trypolanya. 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.

Jerry Nixon
sumber
1
Tentang pendekatan ketiga, apakah Anda yakin ContinueWithpanggilan chaining memiliki hasil yang diharapkan? Menurut pemahaman saya yang kedua ContinueWithakan memeriksa keberhasilan kelanjutan pertama, bukan keberhasilan tugas aslinya.
Theodor Zoulias
1
Cheers @TheodorZoulias, itu mata yang tajam. Tetap.
Jerry Nixon
1
Melontarkan pengecualian untuk kontrol aliran adalah bau kode besar bagi saya - ini akan menambah kinerja Anda
Ian Kemp
Tidak, @IanKemp, itu konsep yang cukup lama. Kompiler telah berevolusi.
Jerry Nixon
4

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:

  1. 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);
     }
  2. Tetapkan async Try-method seperti ini:

     public async Task<AsyncOut<bool, string>> TryReceiveAsync()
     {
         string message;
         bool success;
         // ...
         return (success, message);
     }
  3. 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.

Michael Gehling
sumber
Ini adalah solusi yang sangat cerdas!
Theodor Zoulias
2

Batasan asyncmetode yang tidak menerima outparameter hanya berlaku untuk metode async yang dihasilkan oleh kompiler, ini dinyatakan dengan asynckata kunci. Itu tidak berlaku untuk metode async kerajinan tangan. Dengan kata lain dimungkinkan untuk membuat Taskmetode pengembalian menerima outparameter. Misalnya katakanlah kita sudah memiliki ParseIntAsyncmetode yang melempar, dan kami ingin membuat TryParseIntAsyncyang 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 TaskCompletionSourcedan ContinueWithmetode ini agak canggung, tetapi tidak ada pilihan lain karena kita tidak dapat menggunakan awaitkata 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 TaskCompletionSourcemasih diperlukan untuk outparameter. Ada kemungkinan bahwa outparameter 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, GetRawDataAsyncdan FilterDataAsyncitu disebut berturut-turut. The outparameter selesai pada penyelesaian metode kedua. The GetDataAsyncMetode dapat digunakan seperti ini:

var data = await GetDataAsync("http://example.com", out var rawDataLength);
Console.WriteLine($"Data: {data}");
Console.WriteLine($"RawDataLength: {await rawDataLength}");

Menunggu datasebelum menunggu rawDataLengthadalah penting dalam contoh sederhana ini, karena dalam kasus pengecualian outparameter tidak akan pernah selesai.

Theodor Zoulias
sumber
1
Ini adalah solusi yang sangat bagus untuk beberapa kasus.
Jerry Nixon
1

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):
}
Paul Marangoni
sumber
Anda tidak memerlukan NuGet jika menggunakan .net-4.7 atau netstandard-2.0.
binki
Hei, kamu benar! Saya baru saja menghapus paket NuGet itu dan masih berfungsi. Terima kasih!
Paul Marangoni
1

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/

Jpsy
sumber
-2

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
Payam Buroumand
sumber
Jangan pernah gunakan .Hasil. Ini anti-pola. Terima kasih!
Ben