Ответы:
Вы не можете иметь асинхронные методы с параметрами ref
или out
.
Лучиан Висчик объясняет, почему это невозможно в этом потоке MSDN: http://social.msdn.microsoft.com/Forums/en-US/d2f48a52-e35a-4948-844d-828a1a6deb74/why-async-methods-cannot-have -REF или-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);
Этот конкретный подход похож на метод Try, который 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
метод синхронизации, возвращающий только a 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-paradigm ...
Для меня важно, что я могу вызывать метод Try в одном предложении if, и мне не нужно предварительно определять переменные out, но я могу сделать это in-line, как в следующем примере:
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);
}
Определите асинхронный Try-метод следующим образом:
public async Task<AsyncOut<bool, string>> TryReceiveAsync()
{
string message;
bool success;
// ...
return (success, message);
}
Вызовите асинхронный Try-метод следующим образом:
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
and 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 (библиотека параллельных задач) вместо прямого, используя ключевое слово 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