Відповіді:
Ви не можете мати методи асинхронізації з параметрами ref
або out
параметрами.
Лучан Віщик пояснює, чому це неможливо в потоці MSDN: http://social.msdn.microsoft.com/Forums/en-US/d2f48a52-e35a-4948-844d-828a1a6deb74/why-async-methods-cannot-have -ref-or-out-параметри
Що стосується того, чому методи асинхронізації не підтримують параметри зовнішніх посилань? (чи параметри ref?) Це обмеження CLR. Ми вирішили реалізувати методи асинхронізації аналогічно методам ітератора - тобто через компілятор, який перетворює метод у об'єкт стан-машина. CLR не має безпечного способу зберігати адресу "вихідного параметра" або "опорного параметра" як поле об'єкта. Єдиний спосіб підтримувати параметри зовнішньої посилання, якби функцію асинхронізації виконував перезапис CLR низького рівня замість компілятора-переписання. Ми розглянули цей підхід, і для цього було багато чого, але це в кінцевому підсумку було б таким дорогим, що б цього не сталося.
Типовим вирішенням цієї ситуації є те, щоб метод асинхронізації повернув замість цього кортеж. Ви можете переписати свій метод як такий:
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
альтернативу. Дуже корисний.
Tuple
. : P
Ви не можете мати ref
або out
параметри в async
методах (як уже зазначалося).
Це кричить про деяке моделювання даних, що рухаються:
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;
}
Ви отримуєте можливість легшого використання коду, плюс він є більш читабельним, ніж змінні чи кортежі.
Рішення C # 7 + полягає у використанні неявного синтаксису кортежу.
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."
}));
}
результат повернення використовує визначені методом підписи імена властивостей. наприклад:
var foo = await TryLogin(request);
if (foo.IsSuccess)
return foo.Result;
Алекс зробив чудову крапку щодо читабельності. Еквівалентно, що функція також є достатньо інтерфейсом для визначення типів, які повертаються, і ви також отримуєте змістовні імена змінних.
delegate void OpDelegate(int op);
Task<bool> GetDataTaskAsync(OpDelegate callback)
{
bool canGetData = true;
if (canGetData) callback(5);
return Task.FromResult(canGetData);
}
Абоненти надають лямбда (або названу функцію), а intellisense допомагає, скопіювавши імена змінних з делегата.
int myOp;
bool result = await GetDataTaskAsync(op => myOp = op);
Цей конкретний підхід схожий на метод "Спробувати", який myOp
встановлюється, якщо результат методу true
. Інакше тебе не хвилює myOp
.
Однією приємною особливістю out
параметрів є те, що вони можуть використовуватися для повернення даних, навіть коли функція кидає виняток. Я думаю, що найближчим еквівалентом цього async
методу було б використання нового об'єкта для зберігання даних, до яких async
можуть звертатися і метод, і виклик. Іншим способом було б передати делегата, як було запропоновано в іншій відповіді .
Зауважте, що жоден із цих методів не матиме жодного з видів примусового виконання компілятора out
. Тобто компілятор не вимагатиме від вас встановлення значення на спільному об'єкті або виклику переданого в делегат.
Ось приклад реалізації з використанням спільного об'єкта для імітації ref
та out
для використання з async
методами та іншими різними сценаріями, де ref
та out
недоступні:
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.");
}
}
Я люблю Try
візерунок. Це охайний візерунок.
if (double.TryParse(name, out var result))
{
// handle success
}
else
{
// handle error
}
Але, з цим складно async
. Це не означає, що у нас немає реальних варіантів. Ось три основні підходи, які ви можете розглянути для async
методів у квазі-версії Try
шаблону.
Це найбільше схоже на Try
метод синхронізації лише повернення tuple
замість параметра bool
з out
параметром, який, як ми всі знаємо, заборонено в C #.
var result = await DoAsync(name);
if (result.Success)
{
// handle success
}
else
{
// handle error
}
З методом , який повертається true
з false
і ніколи не кидає exception
.
Пам'ятайте, що кидання винятку
Try
методом порушує всю мету шаблону.
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);
}
}
Ми можемо використовувати anonymous
методи для встановлення зовнішніх змінних. Це розумний синтаксис, хоча і трохи складний. У малих дозах це добре.
var file = default(StorageFile);
var exception = default(Exception);
if (await DoAsync(name, x => file = x, x => exception = x))
{
// handle success
}
else
{
// handle failure
}
Метод підкоряється основам Try
шаблону, але встановлює out
параметри, що передаються в методах зворотного виклику. Робиться так.
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;
}
}
Тут у мене на думці питання про продуктивність. Але компілятор C # настільки вигадливий, що я думаю, що ви безпечно вибираєте цей варіант, майже напевно.
Що робити, якщо ви просто використовуєте TPL
як розроблено? Ніяких кортежів. Ідея тут полягає в тому, що ми використовуємо винятки для переадресації ContinueWith
на два різні шляхи.
await DoAsync(name).ContinueWith(task =>
{
if (task.Exception != null)
{
// handle fail
}
if (task.Result is StorageFile sf)
{
// handle success
}
});
З методом, який кидає, exception
коли є якісь збої. Це інакше, ніж повернення а boolean
. Це спосіб спілкування зі службою TPL
.
async Task<StorageFile> DoAsync(string fileName)
{
var folder = ApplicationData.Current.LocalCacheFolder;
return await folder.GetFileAsync(fileName);
}
У наведеному вище коді, якщо файл не знайдено, викидається виняток. Це призведе до відмови, ContinueWith
який буде справлятись Task.Exception
у своєму логічному блоці. Акуратно, так?
Слухай, є причина, що ми любимо
Try
викрійку. Це в принципі настільки охайно і читабельно, і, як результат, ретельно. Вибираючи свій підхід, слідкуйте за читальністю. Згадайте наступного розробника, який через 6 місяців і у вас немає відповіді на уточнюючі питання. Ваш код може бути єдиною документацією, яку коли-небудь матиме розробник.
Удачі.
ContinueWith
дзвінків має очікуваний результат? На моє розуміння, другий ContinueWith
перевірить успішність першого продовження, а не успіх початкового завдання.
У мене була така ж проблема, як мені подобається використовувати метод Try-method-pattern, який в основному здається несумісним з парадигмою async-await ...
Для мене важливо те, що я можу викликати Try-метод у межах одного if-clause і не потрібно заздалегідь визначати вихідні змінні раніше, але можу це робити в рядку, як у наступному прикладі:
if (TryReceive(out string msg))
{
// use msg
}
Тому я придумав таке рішення:
Визначте структуру помічника:
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);
}
Визначте асинхронний метод спробу:
public async Task<AsyncOut<bool, string>> TryReceiveAsync()
{
string message;
bool success;
// ...
return (success, message);
}
Виклик методу асинхронізації таким чином:
if ((await TryReceiveAsync()).Out(out string msg))
{
// use msg
}
Для декількох параметрів можна визначити додаткові структури (наприклад, AsyncOut <T, OUT1, OUT2>) або ви можете повернути кортеж.
Обмеження async
методів неприйняття out
параметрів поширюється лише на створені компілятором методи асинхронізації, зазначені за допомогою async
ключового слова. Це не застосовується до асинхронних методів, виготовлених вручну. Іншими словами, можна створити Task
методи повернення, що приймають out
параметри. Наприклад, скажімо, що у нас вже є ParseIntAsync
метод, який кидає, і ми хочемо створити той TryParseIntAsync
, який не кидає. Ми могли б реалізувати це так:
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);
}
Використання методу TaskCompletionSource
і ContinueWith
методу трохи незручно, але іншого варіанту немає, оскільки ми не можемо скористатися зручнимawait
ключове слово всередині цього методу.
Приклад використання:
if (await TryParseIntAsync("-13", out var result))
{
Console.WriteLine($"Result: {await result}");
}
else
{
Console.WriteLine($"Parse failed");
}
Оновлення: Якщо логіка асинхронізації занадто складна для вираження без неї await
, вона може бути інкапсульована всередині вкладеного асинхронного анонімного делегата. TaskCompletionSource
Буде по- як і раніше необхідно для out
параметра. Можливо, що out
параметр можна було виконати до завершення основного завдання, як в наведеному нижче прикладі:
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);
}))();
}
У цьому прикладі передбачається існування трьох асинхронних методів GetResponseAsync
, GetRawDataAsync
і FilterDataAsync
які називаються послідовно. out
Параметр завершуються по завершенню другого методу. GetDataAsync
Метод може бути використаний , як це:
var data = await GetDataAsync("http://example.com", out var rawDataLength);
Console.WriteLine($"Data: {data}");
Console.WriteLine($"RawDataLength: {await rawDataLength}");
В очікуванні data
до того, що очікують rawDataLength
важливо в цьому спрощеному прикладі, так як в разі виключення out
параметр ніколи не буде завершено.
Я думаю, що використання подібних ValueTuples може працювати. Вам слід спочатку додати пакет 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):
}
Ось код відповіді @ dcastro, модифікований для C # 7.0 із названими кортежами та деконструкцією кортежу, що впорядковує позначення:
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);
}
Докладніше про нові названі кортежі, кортежні літерали та кортежні деконструкції див: https://blogs.msdn.microsoft.com/dotnet/2017/03/09/new-features-in-c-7-0/
Для цього можна скористатися TPL (бібліотека паралельних завдань) замість прямого використання ключового слова очікування.
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