Jak napisać metodę asynchroniczną bez parametru out?

176

Chcę napisać metodę asynchroniczną z outparametrem, takim jak ten:

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

Jak to zrobić GetDataTaskAsync?

jesse
źródło

Odpowiedzi:

279

Nie możesz mieć metod asynchronicznych z parametrami reflub out.

Lucian Wischik wyjaśnia, dlaczego nie jest to możliwe w tym wątku MSDN: http://social.msdn.microsoft.com/Forums/en-US/d2f48a52-e35a-4948-844d-828a1a6deb74/why-async-methods-cannot-have -ref-or-out-parameters

Co do tego, dlaczego metody asynchroniczne nie obsługują parametrów zewnętrznych? (lub parametry ref?) To jest ograniczenie CLR. Zdecydowaliśmy się zaimplementować metody asynchroniczne w podobny sposób jak metody iteracyjne - tj. Poprzez kompilator przekształcający metodę w obiekt maszyny stanu. Środowisko CLR nie ma bezpiecznego sposobu przechowywania adresu „parametru wyjściowego” lub „parametru odniesienia” jako pola obiektu. Jedynym sposobem na uzyskanie obsługiwanych parametrów poza odwołaniem byłoby wykonanie funkcji asynchronicznej przez przepisanie środowiska CLR niskiego poziomu zamiast przepisywania przez kompilator. Przeanalizowaliśmy to podejście i wymagało to wielu działań, ale ostatecznie byłoby to tak kosztowne, że nigdy by się nie wydarzyło.

Typowym obejściem tej sytuacji jest zwrócenie przez metodę asynchroniczną krotki. Możesz ponownie napisać swoją metodę jako taką:

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
źródło
10
To nie jest zbyt skomplikowane, ale może powodować zbyt wiele problemów. Jon Skeet wyjaśnił to bardzo dobrze tutaj stackoverflow.com/questions/20868103/…
MuiBienCarlota
3
Dzięki za Tuplealternatywę. Bardzo pomocne.
Luke Vo
19
to jest brzydkie Tuple. : P
tofutim
36
Myślę, że nazwane krotki w C # 7 będą idealnym rozwiązaniem.
orad
3
@orad Szczególnie podoba mi się to: private async Task <(bool success, Job job, string message)> TryGetJobAsync (...)
J. Andrew Laughlin
51

Nie możesz mieć parametrów reflub outw asyncmetodach (jak już wspomniano).

To krzyczy o pewne modelowanie w poruszających się danych:

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

Zyskujesz możliwość łatwiejszego ponownego użycia kodu, a ponadto jest on o wiele bardziej czytelny niż zmienne lub krotki.

Alex
źródło
2
Wolę to rozwiązanie zamiast używania krotki. Bardziej czysty!
MiBol
31

Rozwiązanie C # 7 + polega na użyciu niejawnej składni krotki.

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

zwracany wynik wykorzystuje nazwy właściwości zdefiniowanych w sygnaturach metody. na przykład:

var foo = await TryLogin(request);
if (foo.IsSuccess)
     return foo.Result;
jv_
źródło
12

Alex poruszył wielką kwestię czytelności. Równoważnie funkcja jest również wystarczającym interfejsem, aby zdefiniować zwracane typy, a także uzyskać zrozumiałe nazwy zmiennych.

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

Wywołujące zapewniają lambdę (lub nazwaną funkcję), a funkcja Intellisense pomaga, kopiując nazwy zmiennych od delegata.

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

To szczególne podejście przypomina metodę „Try”, w której myOpjest ustawiana, jeśli wynik metody to true. W przeciwnym razie nie przejmujesz się myOp.

Scott Turner
źródło
9

Jedną z fajnych cech outparametrów jest to, że mogą być używane do zwracania danych nawet wtedy, gdy funkcja zgłasza wyjątek. Myślę, że najbliższym odpowiednikiem zrobienia tego za pomocą asyncmetody byłoby użycie nowego obiektu do przechowywania danych, do których asyncmoże się odnosić zarówno metoda, jak i wywołujący. Innym sposobem byłoby przekazanie delegata zgodnie z sugestią zawartą w innej odpowiedzi .

Zauważ, że żadna z tych technik nie będzie miała żadnego rodzaju wymuszenia ze strony kompilatora, który outma. Oznacza to, że kompilator nie będzie wymagał ustawienia wartości udostępnionego obiektu ani wywołania przekazanego delegata.

Oto przykładowa implementacja korzystająca z udostępnionego obiektu do naśladowania refi outdo użytku z asyncmetodami i innymi różnymi scenariuszami, w których refi outnie są dostępne:

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
źródło
6

Uwielbiam Trywzór. To uporządkowany wzór.

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

Ale to trudne async. To nie znaczy, że nie mamy prawdziwych opcji. Oto trzy podstawowe podejścia, które można rozważyć w przypadku asyncmetod w quasi-wersji Trywzorca.

Podejście 1 - wyprowadź konstrukcję

Wygląda to najbardziej jak Trymetoda synchronizacji zwracająca tylko a tuplezamiast a boolz outparametrem, który, jak wszyscy wiemy, jest niedozwolony w C #.

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

Ze sposobu, który powraca truez falsei nigdy nie zgłasza exception.

Pamiętaj, że rzucenie wyjątku w Trymetodzie łamie cały cel wzorca.

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

Podejście 2 - przekazanie metod wywołania zwrotnego

Możemy użyć anonymousmetod do ustawienia zmiennych zewnętrznych. To sprytna składnia, choć nieco skomplikowana. W małych dawkach jest w porządku.

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

Metoda jest zgodna z podstawami Trywzorca, ale ustawia outparametry przekazywane w metodach wywołania zwrotnego. Robi się to w ten sposób.

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

W mojej głowie pojawia się pytanie dotyczące wydajności. Ale kompilator C # jest tak cholernie inteligentny, że myślę, że możesz bezpiecznie wybrać tę opcję, prawie na pewno.

Podejście 3 - użyj ContinueWith

A co, jeśli po prostu użyjesz zgodnego z TPLprojektem? Żadnych krotek. Chodzi o to, że używamy wyjątków, aby przekierowywać ContinueWithna dwie różne ścieżki.

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

Z metodą, która rzuca, exceptionkiedy pojawia się jakikolwiek błąd. To coś innego niż zwrócenie pliku boolean. To sposób na komunikację z TPL.

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

W powyższym kodzie, jeśli plik nie zostanie znaleziony, zostanie zgłoszony wyjątek. Spowoduje to wywołanie błędu, ContinueWithktóry będzie obsługiwany Task.Exceptionw jego bloku logicznym. Schludnie, co?

Posłuchaj, jest powód, dla którego uwielbiamy ten Trywzór. Zasadniczo jest tak schludny i czytelny, a co za tym idzie, łatwy w utrzymaniu. Gdy wybierzesz swoje podejście, monitoruj czytelność. Pamiętaj o kolejnym deweloperze, który za 6 miesięcy nie będzie musiał odpowiadać na wyjaśniające pytania. Twój kod może być jedyną dokumentacją, jaką programista kiedykolwiek będzie miał.

Powodzenia.

Jerry Nixon
źródło
1
Jeśli chodzi o trzecie podejście, czy jesteś pewien, że łączenie ContinueWithpołączeń daje oczekiwany wynik? Zgodnie z moim zrozumieniem, druga ContinueWithbędzie sprawdzać powodzenie pierwszej kontynuacji, a nie powodzenie pierwotnego zadania.
Theodor Zoulias
1
Pozdrawiam @TheodorZoulias, to bystre oko. Naprawiony.
Jerry Nixon
1
Rzucanie wyjątków dla kontroli przepływu to dla mnie ogromny zapach kodu - to podbije twoją wydajność.
Ian Kemp
Nie, @IanKemp, to dość stara koncepcja. Kompilator ewoluował.
Jerry Nixon
4

Miałem ten sam problem, co lubię, używając wzorca metody Try, który zasadniczo wydaje się niekompatybilny z paradygmatem async-await ...

Ważne dla mnie jest to, że mogę wywołać metodę Try w pojedynczej klauzuli if i nie muszę wcześniej definiować zmiennych wyjściowych, ale mogę to zrobić w linii, jak w poniższym przykładzie:

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

Więc wymyśliłem następujące rozwiązanie:

  1. Zdefiniuj strukturę pomocniczą:

     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. Zdefiniuj metodę async Try w następujący sposób:

     public async Task<AsyncOut<bool, string>> TryReceiveAsync()
     {
         string message;
         bool success;
         // ...
         return (success, message);
     }
  3. Wywołaj metodę async Try w następujący sposób:

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

Dla wielu parametrów wyjściowych można zdefiniować dodatkowe struktury (np. AsyncOut <T, OUT1, OUT2>) lub zwrócić krotkę.

Michael Gehling
źródło
To bardzo sprytne rozwiązanie!
Theodor Zoulias
2

Ograniczenie asyncmetod, które nie akceptują outparametrów, dotyczy tylko metod asynchronicznych generowanych przez kompilator, które zostały zadeklarowane za pomocą asyncsłowa kluczowego. Nie dotyczy to ręcznie tworzonych metod asynchronicznych. Innymi słowy, można tworzyć Taskmetody zwracające akceptujące outparametry. Na przykład powiedzmy, że mamy już ParseIntAsyncmetodę, która rzuca i chcemy stworzyć taką TryParseIntAsync, która nie rzuca. Moglibyśmy to zaimplementować w ten sposób:

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

Używanie TaskCompletionSourcei ten ContinueWithsposób jest nieco niewygodne, ale nie ma innej opcji, ponieważ nie mogą korzystać z wygodnego awaitsłowa kluczowego wewnątrz tej metody.

Przykład użycia:

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

Aktualizacja: jeśli logika asynchroniczna jest zbyt złożona, aby można ją było wyrazić bez await, można ją hermetyzować wewnątrz zagnieżdżonego asynchronicznego anonimowego delegata. TaskCompletionSourceNadal będzie potrzebna dla outparametru. Możliwe, że outparametr mógłby zostać uzupełniony przed zakończeniem głównego zadania, jak w poniższym przykładzie:

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

W tym przykładzie zakłada istnienie trzech metod asynchronicznych GetResponseAsync, GetRawDataAsynci FilterDataAsyncktóre są nazywane w drugim. outParametru wykonany w zakończeniu drugiego sposobu. GetDataAsyncMetoda może być stosowana tak:

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

Oczekiwanie na dataprzed oczekiwaniem na znak rawDataLengthjest ważne w tym uproszczonym przykładzie, ponieważ w przypadku wyjątku outparametr nigdy nie zostanie uzupełniony.

Theodor Zoulias
źródło
1
To bardzo fajne rozwiązanie w niektórych przypadkach.
Jerry Nixon
1

Myślę, że takie użycie ValueTuples może działać. Musisz jednak najpierw dodać pakiet ValueTuple NuGet:

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
źródło
Nie potrzebujesz NuGet, jeśli używasz .net-4.7 lub netstandard-2.0.
binki
Hej, masz rację! Właśnie odinstalowałem ten pakiet NuGet i nadal działa. Dzięki!
Paul Marangoni
1

Oto kod odpowiedzi @ dcastro zmodyfikowany dla C # 7.0 z nazwanymi krotkami i dekonstrukcją krotek, co usprawnia notację:

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

Aby uzyskać szczegółowe informacje na temat nowych nazwanych krotek, literałów krotek i dekonstrukcji krotek, zobacz: https://blogs.msdn.microsoft.com/dotnet/2017/03/09/new-features-in-c-7-0/

Jpsy
źródło
-2

Możesz to zrobić za pomocą TPL (biblioteki równoległej zadań) zamiast bezpośrednio za pomocą słowa kluczowego await.

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
źródło
Nigdy nie używaj .Result. To anty-wzór. Dzięki!
Ben