Odpowiedzi:
Nie możesz mieć metod asynchronicznych z parametrami ref
lub 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);
}
Tuple
alternatywę. Bardzo pomocne.
Tuple
. : P
Nie możesz mieć parametrów ref
lub out
w async
metodach (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.
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;
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 myOp
jest ustawiana, jeśli wynik metody to true
. W przeciwnym razie nie przejmujesz się myOp
.
Jedną z fajnych cech out
parametró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ą async
metody byłoby użycie nowego obiektu do przechowywania danych, do których async
moż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 out
ma. 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 ref
i out
do użytku z async
metodami i innymi różnymi scenariuszami, w których ref
i out
nie 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.");
}
}
Uwielbiam Try
wzó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 async
metod w quasi-wersji Try
wzorca.
Wygląda to najbardziej jak Try
metoda synchronizacji zwracająca tylko a tuple
zamiast a bool
z out
parametrem, 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 true
z false
i nigdy nie zgłasza exception
.
Pamiętaj, że rzucenie wyjątku w
Try
metodzie ł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);
}
}
Możemy użyć anonymous
metod 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 Try
wzorca, ale ustawia out
parametry 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.
A co, jeśli po prostu użyjesz zgodnego z TPL
projektem? Żadnych krotek. Chodzi o to, że używamy wyjątków, aby przekierowywać ContinueWith
na 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, exception
kiedy 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, ContinueWith
który będzie obsługiwany Task.Exception
w jego bloku logicznym. Schludnie, co?
Posłuchaj, jest powód, dla którego uwielbiamy ten
Try
wzó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.
ContinueWith
połączeń daje oczekiwany wynik? Zgodnie z moim zrozumieniem, druga ContinueWith
będzie sprawdzać powodzenie pierwszej kontynuacji, a nie powodzenie pierwotnego zadania.
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:
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);
}
Zdefiniuj metodę async Try w następujący sposób:
public async Task<AsyncOut<bool, string>> TryReceiveAsync()
{
string message;
bool success;
// ...
return (success, message);
}
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ę.
Ograniczenie async
metod, które nie akceptują out
parametrów, dotyczy tylko metod asynchronicznych generowanych przez kompilator, które zostały zadeklarowane za pomocą async
słowa kluczowego. Nie dotyczy to ręcznie tworzonych metod asynchronicznych. Innymi słowy, można tworzyć Task
metody zwracające akceptujące out
parametry. Na przykład powiedzmy, że mamy już ParseIntAsync
metodę, 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 TaskCompletionSource
i ten ContinueWith
sposób jest nieco niewygodne, ale nie ma innej opcji, ponieważ nie mogą korzystać z wygodnego await
sł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. TaskCompletionSource
Nadal będzie potrzebna dla out
parametru. Możliwe, że out
parametr 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
, GetRawDataAsync
i FilterDataAsync
które są nazywane w drugim. out
Parametru wykonany w zakończeniu drugiego sposobu. GetDataAsync
Metoda 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 data
przed oczekiwaniem na znak rawDataLength
jest ważne w tym uproszczonym przykładzie, ponieważ w przypadku wyjątku out
parametr nigdy nie zostanie uzupełniony.
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):
}
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/
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