Antworten:
Sie können keine asynchronen Methoden mit ref
oder 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);
}
Tuple
Alternative. Sehr hilfreich.
Tuple
. : P
Sie können noch nicht ref
oder out
Parameter 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.
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;
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 myOp
wird, ob das Methodenergebnis vorliegt true
. Ansonsten interessiert es dich nicht myOp
.
Eine nette Eigenschaft von out
Parametern 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 async
wäre, ein neues Objekt zu verwenden, um die Daten zu speichern, auf die sowohl die async
Methode 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 out
hat. 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 ref
und out
für die Verwendung mit async
Methoden und verschiedenen anderen Szenarien , in denen ref
und out
nicht 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.");
}
}
Ich liebe das Try
Muster. 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 async
Methoden in einer Quasi-Version des Try
Musters berücksichtigen können .
Dies sieht am ehesten nach einer Synchronisierungsmethode aus Try
, die nur a tuple
anstelle von a bool
mit einem out
Parameter 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 true
von false
und nie werfen exception
.
Denken Sie daran, dass das Auslösen einer Ausnahme in einer
Try
Methode 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);
}
}
Wir können anonymous
Methoden 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 Try
Musters, setzt jedoch out
Parameter, 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.
Was ist, wenn Sie nur das TPL
wie vorgesehen verwenden? Keine Tupel. Die Idee hier ist, dass wir Ausnahmen verwenden, um ContinueWith
auf 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 exception
Fehler 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.Exception
in seinem Logikblock behandelt wird. Ordentlich, was?
Hören Sie, es gibt einen Grund, warum wir das
Try
Muster 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.
ContinueWith
Anrufen das erwartete Ergebnis hat? Nach meinem Verständnis wird die zweite ContinueWith
den Erfolg der ersten Fortsetzung überprüfen, nicht den Erfolg der ursprünglichen Aufgabe.
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:
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);
}
Definieren Sie die asynchrone Try-Methode wie folgt:
public async Task<AsyncOut<bool, string>> TryReceiveAsync()
{
string message;
bool success;
// ...
return (success, message);
}
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.
Die Einschränkung der async
Methoden, die keine out
Parameter akzeptieren , gilt nur für die vom Compiler generierten asynchronen Methoden, die mit dem async
Schlüsselwort deklariert wurden. Dies gilt nicht für handgefertigte asynchrone Methoden. Mit anderen Worten, es ist möglich, Task
Rückgabemethoden zu erstellen , die out
Parameter akzeptieren . Nehmen wir zum Beispiel an, wir haben bereits eine ParseIntAsync
Methode, 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 TaskCompletionSource
und- ContinueWith
Methode 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. TaskCompletionSource
Für den out
Parameter wird weiterhin A benötigt . Es ist möglich, dass der out
Parameter 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
, GetRawDataAsync
und FilterDataAsync
daß in Aufeinanderfolge genannt. Der out
Parameter wird nach Abschluss der zweiten Methode abgeschlossen. Die GetDataAsync
Methode 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, data
auf das Warten zu warten rawDataLength
, da der out
Parameter im Falle einer Ausnahme niemals vervollständigt wird.
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):
}
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/
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