Out parametresi ile bir async yöntemi nasıl yazılır?


176

Aşağıdaki outgibi bir parametre ile bir zaman uyumsuz yöntem yazmak istiyorum :

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

Bunu nasıl yapabilirim GetDataTaskAsync?

Yanıtlar:


279

refVeya outparametresiyle eşzamansız yöntemleriniz olamaz .

Lucian Wischik, bunun bu MSDN iş parçacığında neden mümkün olmadığını açıklıyor: http://social.msdn.microsoft.com/Forums/en-US/d2f48a52-e35a-4948-844d-828a1a6deb74/why-async-methods-cannot-have rEF-ya-dışı parametreleri

Neden zaman uyumsuz yöntemler referans dışı parametreleri desteklemiyor? (veya ref parametreleri?) Bu CLR'nin bir sınırlamasıdır. Eşzamansız yöntemleri yineleyici yöntemlere benzer şekilde uygulamayı seçtik - yani yöntemi bir durum-makine-nesnesine dönüştüren derleyici aracılığıyla. CLR'nin bir "out parametresi" veya "referans parametresi" adresini bir nesnenin alanı olarak saklamak için güvenli bir yolu yoktur. Destek dışı parametreleri desteklemenin tek yolu, eşzamansız özelliğin bir derleyici yeniden yazma yerine düşük düzeyli bir CLR yeniden yazma işlemi tarafından yapılmasıdır. Bu yaklaşımı inceledik ve bunun için çok şey vardı, ama sonuçta o kadar maliyetli olacaktı ki asla gerçekleşmeyecekti.

Bu durum için tipik bir çözüm, bunun yerine async yönteminin bir Tuple döndürmesini sağlamaktır. Yönteminizi şu şekilde yeniden yazabilirsiniz:

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);
}

10
Çok karmaşık olmaktan çok, bu çok fazla sorun yaratabilir. Jon Skeet burada çok iyi açıkladı stackoverflow.com/questions/20868103/…
MuiBienCarlota

3
TupleAlternatif için teşekkürler . Çok yararlı.
Luke Vo

19
çirkin Tuple. : P
tofutim

36
Ben düşünüyorum dizilerini Named bunun için mükemmel bir çözüm olacaktır C # 7'de.
orad

3
@orad Bunu özellikle beğendim: private async Task <(bool başarı, İş işi, dize mesajı)> TryGetJobAsync (...)
J. Andrew Laughlin

51

Sen olamaz refya outparametreler asyncyöntemlerle (zaten belirtildiği gibi).

Bu, hareket eden verilerde bazı modellemeler için bağırır:

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;
}

Kodunuzu daha kolay bir şekilde yeniden kullanma olanağına sahip olursunuz, ayrıca değişkenlerden veya gruplardan daha okunabilir.


2
Bir Tuple kullanmak yerine bu çözümü tercih ederim. Daha temiz!
MiBol

31

C # 7 + Çözümü örtülü grup sözdizimini kullanmaktır.

    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."
        }));
    }

sonuç döndürme imzası tanımlı özellik adlarını kullanır. Örneğin:

var foo = await TryLogin(request);
if (foo.IsSuccess)
     return foo.Result;

12

Alex okunabilirliğe büyük önem verdi. Aynı şekilde, bir işlev de döndürülmekte olan tür (ler) i tanımlamak için yeterince arabirimdir ve anlamlı değişken adları da alırsınız.

delegate void OpDelegate(int op);
Task<bool> GetDataTaskAsync(OpDelegate callback)
{
    bool canGetData = true;
    if (canGetData) callback(5);
    return Task.FromResult(canGetData);
}

Arayanlar bir lambda (veya adlandırılmış bir işlev) sağlar ve intellisense, değişken adlarını temsilciden kopyalayarak yardımcı olur.

int myOp;
bool result = await GetDataTaskAsync(op => myOp = op);

Bu özel yaklaşım, " myOpSonuç " yöntemi gibidir; burada yöntem sonucu ise ayarlanır true. Aksi takdirde, umurunda değil myOp.


9

outParametrelerin güzel bir özelliği, bir işlev bir istisna attığında bile veri döndürmek için kullanılabilmeleridir. Ben bir asyncyöntemle bunu yapmak için en yakın eşdeğer asyncyöntem ve arayan başvurabilir veri tutmak için yeni bir nesne kullanmak olacağını düşünüyorum . Başka bir yol , başka bir cevapta önerildiği gibi bir delege geçmek olacaktır .

Bu tekniklerin hiçbirinin sahip olduğu derleyiciden herhangi bir tür uygulamaya sahip olmayacağını unutmayın out. Yani, derleyici, paylaşılan nesne üzerindeki değeri ayarlamanıza veya aktarılan bir temsilci çağırmanıza gerek duymaz.

İşte imitate için paylaşılan nesnesini kullanarak bir örnek uygulama refve outbirlikte kullanılmak üzere asyncyöntem ve diğer çeşitli senaryolar refve outkullanılamaz:

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.");
    }
}

6

TryDeseni seviyorum . Düzenli bir model.

if (double.TryParse(name, out var result))
{
    // handle success
}
else
{
    // handle error
}

Ancak, bu zor async. Bu, gerçek seçeneklerimiz olmadığı anlamına gelmez. asyncDesenin yarı versiyonundaki yöntemler için düşünebileceğiniz üç temel yaklaşım şunlardır Try.

Yaklaşım 1 - Yapı Çıktısı

Bu, çoğu zaman C # 'de izin verilmediğini bildiğimiz bir parametreyle yerine bir Trydöndüren bir senkronizasyon yöntemine benziyor .tupleboolout

var result = await DoAsync(name);
if (result.Success)
{
    // handle success
}
else
{
    // handle error
}

Döner bir yöntemle trueait falseve asla bir atar exception.

Unutmayın, bir Tryyönteme istisna atmak , desenin tüm amacını kırar.

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);
    }
}

Yaklaşım 2 - Geri arama yöntemlerinde geçiş

anonymousDış değişkenleri belirlemek için yöntemler kullanabiliriz . Zeki sözdizimi biraz karmaşık olsa da. Küçük dozlarda, sorun değil.

var file = default(StorageFile);
var exception = default(Exception);
if (await DoAsync(name, x => file = x, x => exception = x))
{
    // handle success
}
else
{
    // handle failure
}

Yöntem, Trydesenin temellerine uyar, ancak outgeri arama yöntemlerinde iletilecek parametreleri ayarlar . Bu böyle yapılır.

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;
    }
}

Aklımda burada performansla ilgili bir soru var. Ancak, C # derleyicisi o kadar akıllı ki, bu seçeneği seçerken neredeyse güvende olduğunuzu düşünüyorum.

Yaklaşım 3 - ContinueWith kullanın

Ya sadece TPLtasarlandığı gibi kullanırsanız ? Tuples yok. Buradaki fikir ContinueWith, iki farklı yola yönlendirmek için istisnalar kullanmamızdır .

await DoAsync(name).ContinueWith(task =>
{
    if (task.Exception != null)
    {
        // handle fail
    }
    if (task.Result is StorageFile sf)
    {
        // handle success
    }
});

exceptionHerhangi bir hata olduğunda bir zaman atan bir yöntemle . Bu, a döndürmekten farklıdır boolean. İle iletişim kurmanın bir yolu TPL.

async Task<StorageFile> DoAsync(string fileName)
{
    var folder = ApplicationData.Current.LocalCacheFolder;
    return await folder.GetFileAsync(fileName);
}

Yukarıdaki kodda, dosya bulunamazsa bir istisna atılır. Bu , mantık bloğunda ContinueWithişlenecek hatayı başlatır Task.Exception. Düzgün, ha?

Dinle, Trykalıbı sevmemizin bir nedeni var . Temelde çok temiz ve okunabilir ve sonuç olarak bakım yapılabilir. Yaklaşımınızı seçerken, okunabilirlik için bekçi köpeği. 6 ay içinde açıklayıcı soruları cevaplamanıza gerek olmayan bir sonraki geliştiriciyi hatırlayın. Kodunuz, bir geliştiricinin sahip olacağı tek belge olabilir.

İyi şanslar.


1
Üçüncü yaklaşım hakkında, ContinueWithçağrıları zincirlemenin beklenen sonuca sahip olduğundan emin misiniz ? Anladığım kadarıyla, ikincisi ContinueWith, ilk görevin başarısını değil, orijinal görevin başarısını kontrol edecektir.
Theodor Zoulias

1
Şerefe @TheodorZoulias, bu keskin bir göz. Sabit.
Jerry Nixon

1
Akış kontrolü için istisnalar atmak benim için büyük bir kod kokusu - performansınızı depolayacak.
Ian Kemp

Hayır, @IanKemp, bu oldukça eski bir kavram. Derleyici gelişti.
Jerry Nixon

4

Temelde async-await-paradigma ile uyumsuz gibi görünen Try-method-desenini kullanmak gibi aynı sorun vardı ...

Benim için önemli olan, Try-yöntemini tek bir if-cümlesi içinde çağırabiliyorum ve daha önce out değişkenlerini önceden tanımlamak zorunda değilim, ancak aşağıdaki örnekte olduğu gibi satır içi yapabilirim:

if (TryReceive(out string msg))
{
    // use msg
}

Bu yüzden aşağıdaki çözümü buldum:

  1. Bir yardımcı yapı tanımlayın:

     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. Async Try-yöntemini şu şekilde tanımlayın:

     public async Task<AsyncOut<bool, string>> TryReceiveAsync()
     {
         string message;
         bool success;
         // ...
         return (success, message);
     }
  3. Async Try-yöntemini şöyle çağırın:

     if ((await TryReceiveAsync()).Out(out string msg))
     {
         // use msg
     }

Çoklu çıkış parametreleri için ek yapılar tanımlayabilirsiniz (örn. AsyncOut <T, OUT1, OUT2>) veya bir demet döndürebilirsiniz.


Bu çok akıllı bir çözüm!
Theodor Zoulias

2

Parametreleri asynckabul etmeyen yöntemlerin sınırlandırılması outyalnızca derleyici tarafından oluşturulan ve bunlar asyncanahtar sözcükle bildirilen zaman uyumsuz yöntemler için geçerlidir . El yapımı asenkron yöntemler için geçerli değildir. Başka bir deyişle, parametreleri Taskkabul eden geri dönen yöntemler oluşturmak mümkündür out. Örneğin, zaten birParseIntAsync fırlatma yöntemimiz olduğunu ve fırlatmayan yöntem oluşturmak istediğimizi TryParseIntAsyncvarsayalım. Bunu şu şekilde uygulayabiliriz:

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);
}

Kullanmak TaskCompletionSourceVe ContinueWithyöntemini biraz gariptir, ancak awaitbu yöntemin içinde uygun anahtar kelimeyi kullanamadığımız için başka bir seçenek yoktur .

Kullanım örneği:

if (await TryParseIntAsync("-13", out var result))
{
    Console.WriteLine($"Result: {await result}");
}
else
{
    Console.WriteLine($"Parse failed");
}

Güncelleme:await Eşzamansız mantık olmadan ifade edilemeyecek kadar karmaşıksa , o zaman iç içe bir eşzamansız anonim delege içinde kapsüllenebilir. Parametre TaskCompletionSourceiçin hala A gereklidir out. Aşağıdaki outörnekte olduğu gibi , parametrenin ana görevin tamamlanmasından önce tamamlanması mümkündür :

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);
    }))();
}

Bu örnek, üç eşzamansız yöntemin varlığını varsayar GetResponseAsync , GetRawDataAsyncve FilterDataAsyncbu arka arkaya denir. outParametresi ikinci yöntemin tamamlanmasından tamamlanır. GetDataAsyncYöntem bu gibi kullanılabilir:

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

Bekliyor data önce beklemek rawDataLengthbu basitleştirilmiş örnekte önemlidir, çünkü bir istisna durumunda outparametre asla tamamlanmayacaktır.


1
Bu, bazı durumlar için çok güzel bir çözümdür.
Jerry Nixon

1

Bunun gibi ValueTuples kullanarak işe yarayabilir düşünüyorum. Yine de ValueTuple NuGet paketini eklemelisiniz:

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):
}

.Net-4.7 veya netstandard-2.0 kullanıyorsanız NuGet'e ihtiyacınız yoktur.
binki

Hey, haklısın! Sadece NuGet paketini kaldırdım ve hala çalışıyor. Teşekkürler!
Paul Marangoni

1

@ Dcastro'nun C # 7.0 için değiştirilmiş ve tuples ve tuple yapısökümüyle değiştirilmiş yanıtı gösterimi düzenleyen kod kodu:

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);
}

Yeni adlandırılmış tuples, tuple değişmezleri ve tuple yapısökümleri hakkında ayrıntılar için bkz. Https://blogs.msdn.microsoft.com/dotnet/2017/03/09/new-features-in-c-7-0/


-2

Bunu, doğrudan await anahtar sözcüğünü kullanmak yerine TPL (görev paralel kitaplığı) kullanarak yapabilirsiniz.

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

Asla .Result kullanmayın. Bu bir anti-desen. Teşekkürler!
Ben
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.