Я хочу написать метод async с параметром out
, например:
public async void Method1()
{
int op;
int result = await GetDataTaskAsync(out op);
}
Как это сделать в GetDataTaskAsync
?
Я хочу написать метод async с параметром out
, например:
public async void Method1()
{
int op;
int result = await GetDataTaskAsync(out op);
}
Как это сделать в GetDataTaskAsync
?
У вас не может быть асинхронных методов с параметрами ref
или out
.
Lucian Wischik объясняет, почему это невозможно в этой ветке MSDN: http://social.msdn.microsoft.com/Forums/en-US/d2f48a52-e35a-4948-844d-828a1a6deb74/why-async-methods-cannot-have-ref-or-out-parameters
Что касается того, почему асинхронные методы не поддерживают параметры вне ссылки? (или параметры 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);
}
Вы не можете иметь параметры 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;
}
Вы получаете возможность повторно использовать свой код более легко, а также более читабельны, чем переменные или кортежи.
Решением С# 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);
}
Абоненты предоставляют lambda (или именованную функцию), а intellisense помогает, копируя имена (имена) переменных из делегата.
int myOp;
bool result = await GetDataTaskAsync(op => myOp = op);
Этот конкретный подход похож на метод "Try", где myOp
устанавливается, если результат метода равен true
. В противном случае вы не заботитесь о myOp
.
Одна приятная особенность параметров out
заключается в том, что они могут использоваться для возврата данных, даже если функция генерирует исключение. Я думаю, что ближайший эквивалент этого метода с помощью метода async
будет использовать новый объект для хранения данных, которые могут ссылаться на метод async
и вызывающий. Другой способ - передать делегат, как предложено в другом ответе.
Обратите внимание, что ни один из этих методов не будет иметь какого-либо принудительного исполнения от компилятора, который имеет out
. I.e., компилятор не требует, чтобы вы установили значение для общего объекта или вызвали переданный в делегате.
Вот пример реализации с использованием общего объекта для имитации ref
и out
для использования с методами async
и другими различными сценариями, в которых доступны ref
и out
arent:
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
, который, как мы все знаем, недопустим в С#.
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;
}
}
В моей голове возник вопрос о производительности. Но компилятор С# настолько чертовски умен, что я думаю, что вы можете выбрать этот вариант почти наверняка.
Что если вы просто используете 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 месяцев так и не даст вам ответить на уточняющие вопросы. Ваш код может быть единственной документацией, которую когда-либо будет иметь разработчик.
Удачи.
Я думаю, что использование 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, модифицированный для С# 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/
У меня была та же проблема, что и при использовании шаблона Try-method-pattern, который в принципе кажется несовместимым с async-await-paradigm...
Для меня важно, чтобы я мог вызывать метод Try в одном предложении if, и мне не нужно предварительно определять исходные переменные, но я могу сделать это in-line, как в следующем примере:
if (TryReceive(out string msg))
{
// use msg
}
Итак, я пришел к следующему решению:
Определите вспомогательную структуру:
public struct AsyncOutResult<T, OUT>
{
T returnValue;
OUT result;
public AsyncOutResult(T returnValue, OUT result)
{
this.returnValue = returnValue;
this.result = result;
}
public T Result(out OUT result)
{
result = this.result;
return returnValue;
}
}
Определите асинхронный Try-метод следующим образом:
public async Task<AsyncOutResult<bool, string>> TryReceiveAsync()
{
string message;
bool success;
// ...
return new AsyncOutResult<bool, string>(success, message);
}
Вызовите асинхронный Try-метод следующим образом:
if ((await TryReceiveAsync()).Result(out T msg))
{
// use msg
}
Если вам нужно несколько параметров out, конечно, вы можете определить дополнительные структуры, например:
public struct AsyncOutResult<T, OUT1, OUT2>
{
T returnValue;
OUT1 result1;
OUT2 result2;
public AsyncOutResult(T returnValue, OUT1 result1, OUT2 result2)
{
this.returnValue = returnValue;
this.result1 = result1;
this.result2 = result2;
}
public T Result(out OUT1 result1, out OUT2 result2)
{
result1 = this.result1;
result2 = this.result2;
return returnValue;
}
}
Ограничение методов 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
никогда не будет завершен.