Wie schreibe ich eine asynchrone Methode ohne Parameter?

175

Ich möchte eine asynchrone Methode mit einem outParameter wie folgt schreiben :

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

Wie mache ich das in GetDataTaskAsync?

Jesse
quelle

Antworten:

278

Sie können keine asynchronen Methoden mit refoder habenout Parameter verwenden.

Lucian Wischik erklärt, warum dies in diesem MSDN-Thread nicht möglich ist: http://social.msdn.microsoft.com/Forums/en-US/d2f48a52-e35a-4948-844d-828a1a6deb74/why-async-methods-cannot-have -ref-or-out-Parameter

Warum unterstützen asynchrone Methoden keine Out-by-Reference-Parameter? (oder Referenzparameter?) Dies ist eine Einschränkung der CLR. Wir haben uns entschieden, asynchrone Methoden ähnlich wie Iterator-Methoden zu implementieren - dh durch den Compiler, der die Methode in ein State-Machine-Objekt umwandelt. Die CLR hat keine sichere Möglichkeit, die Adresse eines "out-Parameters" oder eines "Referenzparameters" als Feld eines Objekts zu speichern. Die einzige Möglichkeit, Out-by-Reference-Parameter zu unterstützen, besteht darin, dass die asynchrone Funktion durch ein CLR-Umschreiben auf niedriger Ebene anstelle eines Compiler-Umschreibens ausgeführt wird. Wir haben diesen Ansatz untersucht, und es gab viel zu tun, aber es wäre letztendlich so kostspielig gewesen, dass es niemals passiert wäre.

Eine typische Problemumgehung für diese Situation besteht darin, dass die asynchrone Methode stattdessen ein Tupel zurückgibt. Sie können Ihre Methode als solche neu schreiben:

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
quelle
10
Weit davon entfernt, zu komplex zu sein, könnte dies zu viele Probleme verursachen. Jon Skeet hat es hier sehr gut erklärt stackoverflow.com/questions/20868103/…
MuiBienCarlota
3
Danke für die TupleAlternative. Sehr hilfreich.
Luke Vo
19
es ist hässlich zu haben Tuple. : P
tofutim
36
Ich denke, dass Named Tuples in C # 7 die perfekte Lösung dafür sein wird.
Orad
3
@orad Ich mag das besonders: private asynchrone Aufgabe <(Bool Erfolg, Job Job, String Nachricht)> TryGetJobAsync (...)
J. Andrew Laughlin
51

Sie können noch nicht refoder outParameter inasync Methoden haben (wie bereits erwähnt).

Dies schreit nach einer Modellierung in den Daten, die sich bewegen:

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

Sie erhalten die Möglichkeit, Ihren Code einfacher wiederzuverwenden, und er ist weitaus lesbarer als Variablen oder Tupel.

Alex
quelle
2
Ich bevorzuge diese Lösung anstelle eines Tupels. Sauberer!
MiBol
29

Die C # 7 + -Lösung besteht darin, die implizite Tupelsyntax zu verwenden.

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

Das Rückgabeergebnis verwendet die für die Methodensignatur definierten Eigenschaftsnamen. z.B:

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

Alex legte großen Wert auf Lesbarkeit. Entsprechend ist eine Funktion auch eine Schnittstelle genug, um die zurückgegebenen Typen zu definieren, und Sie erhalten auch aussagekräftige Variablennamen.

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

Anrufer stellen ein Lambda (oder eine benannte Funktion) bereit, und Intellisense hilft beim Kopieren der Variablennamen vom Delegaten.

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

Dieser spezielle Ansatz ähnelt einer "Try" -Methode, bei der festgelegt myOpwird, ob das Methodenergebnis vorliegt true. Ansonsten interessiert es dich nicht myOp.

Scott Turner
quelle
9

Eine nette Eigenschaft von outParametern ist, dass sie verwendet werden können, um Daten zurückzugeben, selbst wenn eine Funktion eine Ausnahme auslöst. Ich denke, das nächste Äquivalent dazu asyncwäre, ein neues Objekt zu verwenden, um die Daten zu speichern, auf die sowohl die asyncMethode als auch der Aufrufer verweisen können. Eine andere Möglichkeit wäre, einen Delegierten zu übergeben, wie in einer anderen Antwort vorgeschlagen .

Beachten Sie, dass keine dieser Techniken die vom Compiler erzwungene Durchsetzung outhat. Das heißt, der Compiler verlangt nicht, dass Sie den Wert für das freigegebene Objekt festlegen oder einen übergebenen Delegaten aufrufen.

Hier ist eine Beispiel - Implementierung eines gemeinsames Objekt zu imitieren mit refund outfür die Verwendung mit asyncMethoden und verschiedenen anderen Szenarien , in denen refund outnicht zur Verfügung stehen:

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
quelle
6

Ich liebe das TryMuster. Es ist ein ordentliches Muster.

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

Aber es ist eine Herausforderung mit async. Das heißt nicht, dass wir keine wirklichen Optionen haben. Hier sind die drei Kernansätze, die Sie für asyncMethoden in einer Quasi-Version des TryMusters berücksichtigen können .

Ansatz 1 - Struktur ausgeben

Dies sieht am ehesten nach einer Synchronisierungsmethode aus Try, die nur a tupleanstelle von a boolmit einem outParameter zurückgibt, von dem wir alle wissen, dass er in C # nicht zulässig ist.

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

Bei einem Verfahren , dass die Renditen truevon falseund nie werfen exception.

Denken Sie daran, dass das Auslösen einer Ausnahme in einer TryMethode den gesamten Zweck des Musters verletzt.

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

Ansatz 2 - Rückrufmethoden übergeben

Wir können anonymousMethoden verwenden, um externe Variablen festzulegen. Es ist eine clevere Syntax, wenn auch etwas kompliziert. In kleinen Dosen ist es in Ordnung.

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

Die Methode befolgt die Grundlagen des TryMusters, setzt jedoch outParameter, die in Rückrufmethoden übergeben werden. Es ist so gemacht.

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

Ich habe hier eine Frage zur Leistung. Aber der C # -Compiler ist so verdammt schlau, dass ich mir sicher bin, dass Sie diese Option mit ziemlicher Sicherheit wählen.

Ansatz 3 - Verwenden Sie ContinueWith

Was ist, wenn Sie nur das TPLwie vorgesehen verwenden? Keine Tupel. Die Idee hier ist, dass wir Ausnahmen verwenden, um ContinueWithauf zwei verschiedene Pfade umzuleiten .

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

Mit einer Methode, die einen exceptionFehler auslöst, wenn ein Fehler auftritt. Das ist anders als die Rückgabe von a boolean. Es ist eine Möglichkeit, mit dem zu kommunizieren TPL.

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

Wenn die Datei im obigen Code nicht gefunden wird, wird eine Ausnahme ausgelöst. Dies ruft den Fehler auf ContinueWith, der Task.Exceptionin seinem Logikblock behandelt wird. Ordentlich, was?

Hören Sie, es gibt einen Grund, warum wir das TryMuster lieben . Es ist im Grunde so ordentlich und lesbar und daher wartbar. Achten Sie bei der Auswahl Ihres Ansatzes auf die Lesbarkeit. Denken Sie an den nächsten Entwickler, der in 6 Monaten keine klärenden Fragen beantworten muss. Ihr Code kann die einzige Dokumentation sein, die ein Entwickler jemals haben wird.

Viel Glück.

Jerry Nixon
quelle
1
Sind Sie beim dritten Ansatz sicher, dass die Verkettung von ContinueWithAnrufen das erwartete Ergebnis hat? Nach meinem Verständnis wird die zweite ContinueWithden Erfolg der ersten Fortsetzung überprüfen, nicht den Erfolg der ursprünglichen Aufgabe.
Theodor Zoulias
1
Prost @TheodorZoulias, das ist ein scharfes Auge. Fest.
Jerry Nixon
1
Ausnahmen für die Flusskontrolle zu werfen, ist für mich ein massiver Code-Geruch - es wird Ihre Leistung beeinträchtigen.
Ian Kemp
Nein, @IanKemp, das ist ein ziemlich altes Konzept. Der Compiler hat sich weiterentwickelt.
Jerry Nixon
4

Ich hatte das gleiche Problem, wie ich es mag, das Try-Methoden-Muster zu verwenden, das im Grunde nicht mit dem Async-Wait-Paradigma kompatibel zu sein scheint ...

Wichtig für mich ist, dass ich die Try-Methode innerhalb einer einzelnen if-Klausel aufrufen kann und die out-Variablen vorher nicht vordefinieren muss, sondern wie im folgenden Beispiel inline ausführen kann:

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

Also habe ich folgende Lösung gefunden:

  1. Definieren Sie eine Hilfsstruktur:

     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. Definieren Sie die asynchrone Try-Methode wie folgt:

     public async Task<AsyncOut<bool, string>> TryReceiveAsync()
     {
         string message;
         bool success;
         // ...
         return (success, message);
     }
  3. Rufen Sie die asynchrone Try-Methode folgendermaßen auf:

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

Für mehrere Out-Parameter können Sie zusätzliche Strukturen definieren (z. B. AsyncOut <T, OUT1, OUT2>) oder ein Tupel zurückgeben.

Michael Gehling
quelle
Dies ist eine sehr clevere Lösung!
Theodor Zoulias
2

Die Einschränkung der asyncMethoden, die keine outParameter akzeptieren , gilt nur für die vom Compiler generierten asynchronen Methoden, die mit dem asyncSchlüsselwort deklariert wurden. Dies gilt nicht für handgefertigte asynchrone Methoden. Mit anderen Worten, es ist möglich, TaskRückgabemethoden zu erstellen , die outParameter akzeptieren . Nehmen wir zum Beispiel an, wir haben bereits eine ParseIntAsyncMethode, die wirft, und wir möchten eine Methode erstellen TryParseIntAsync, die nicht wirft. Wir könnten es so umsetzen:

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

Die Verwendung der TaskCompletionSourceund- ContinueWithMethode ist etwas umständlich, aber es gibt keine andere Option, da wir die bequeme nicht verwenden könnenawait Schlüsselwort innerhalb dieser Methode.

Anwendungsbeispiel:

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

Update: Wenn die asynchrone Logik zu komplex ist, um ohne ausgedrückt zu werden await, kann sie in einem verschachtelten asynchronen anonymen Delegaten gekapselt werden. TaskCompletionSourceFür den outParameter wird weiterhin A benötigt . Es ist möglich, dass der outParameter vor Abschluss der Hauptaufgabe abgeschlossen wird, wie im folgenden Beispiel:

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

Dieses Beispiel nimmt die Existenz von drei asynchronen Methoden GetResponseAsync, GetRawDataAsyncund FilterDataAsyncdaß in Aufeinanderfolge genannt. Der outParameter wird nach Abschluss der zweiten Methode abgeschlossen. Die GetDataAsyncMethode könnte folgendermaßen verwendet werden:

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

In diesem vereinfachten Beispiel ist es wichtig, dataauf das Warten zu warten rawDataLength, da der outParameter im Falle einer Ausnahme niemals vervollständigt wird.

Theodor Zoulias
quelle
1
Dies ist in einigen Fällen eine sehr schöne Lösung.
Jerry Nixon
1

Ich denke, dass die Verwendung solcher ValueTuples funktionieren kann. Sie müssen jedoch zuerst das ValueTuple NuGet-Paket hinzufügen:

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
quelle
Sie benötigen das NuGet nicht, wenn Sie .net-4.7 oder netstandard-2.0 verwenden.
Binki
Hey, du hast recht! Ich habe gerade dieses NuGet-Paket deinstalliert und es funktioniert immer noch. Vielen Dank!
Paul Marangoni
1

Hier ist der Code der Antwort von @ dcastro, der für C # 7.0 mit benannten Tupeln und Tupeldekonstruktion geändert wurde, wodurch die Notation optimiert wird:

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

Einzelheiten zu den neuen benannten Tupeln, Tupelliteralen und Tupeldekonstruktionen finden Sie unter: https://blogs.msdn.microsoft.com/dotnet/2017/03/09/new-features-in-c-7-0/

Jpsy
quelle
-2

Sie können dies tun, indem Sie TPL (Task Parallel Library) verwenden, anstatt direkt das Schlüsselwort await zu verwenden.

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
quelle
Verwenden Sie niemals .Result. Es ist ein Anti-Muster. Vielen Dank!
Ben