Как написать асинхронный метод с параметром out?

Я хочу написать метод async с параметром out, например:

public async void Method1()
{
    int op;
    int result = await GetDataTaskAsync(out op);
}

Как это сделать в GetDataTaskAsync?

Ответ 1

У вас не может быть асинхронных методов с параметрами 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);
}

Ответ 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;
}

Вы получаете возможность повторно использовать свой код более легко, а также более читабельны, чем переменные или кортежи.

Ответ 3

Решением С# 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;

Ответ 4

Алекс сделал замечательную точку зрения на удобочитаемость. Эквивалентно, функция также является интерфейсом, достаточным для определения возвращаемого типа (типов), и вы также получаете значащие имена переменных.

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.

Ответ 5

Одна приятная особенность параметров 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.");
    }
}

Ответ 6

Я люблю шаблон Try. Это аккуратный образец.

if (double.TryParse(name, out var result))
{
    // handle success
}
else
{
    // handle error
}

Но это сложно с async. Это не значит, что у нас нет реальных вариантов. Вот три основных подхода, которые вы можете рассмотреть для методов async в квази-версии шаблона Try.

Подход 1 - вывести структуру

Это больше похоже на метод синхронизации 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);
    }
}

Подход 2 - передать методы обратного вызова

Мы можем использовать методы 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;
    }
}

В моей голове возник вопрос о производительности. Но компилятор С# настолько чертовски умен, что я думаю, что вы можете выбрать этот вариант почти наверняка.

Подход 3 - используйте ContinueWith

Что если вы просто используете 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 месяцев так и не даст вам ответить на уточняющие вопросы. Ваш код может быть единственной документацией, которую когда-либо будет иметь разработчик.

Удачи.

Ответ 7

Я думаю, что использование 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):
}

Ответ 8

Здесь код ответа @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/

Ответ 9

У меня была та же проблема, что и при использовании шаблона Try-method-pattern, который в принципе кажется несовместимым с async-await-paradigm...

Для меня важно, чтобы я мог вызывать метод Try в одном предложении if, и мне не нужно предварительно определять исходные переменные, но я могу сделать это in-line, как в следующем примере:

if (TryReceive(out string msg))
{
    // use msg
}

Итак, я пришел к следующему решению:

  1. Определите вспомогательную структуру:

    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;
        }
    }
    
  2. Определите асинхронный Try-метод следующим образом:

    public async Task<AsyncOutResult<bool, string>> TryReceiveAsync()
    {
        string message;
        bool success;
        // ...
        return new AsyncOutResult<bool, string>(success, message);
    }
    
  3. Вызовите асинхронный 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;
    }
}

Ответ 10

Ограничение методов 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 никогда не будет завершен.