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

176

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

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

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

Jesse
источник

Ответы:

279

Вы не можете иметь асинхронные методы с параметрами 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);
}
dcastro
источник
10
Это далеко не так сложно, это может создать слишком много проблем. Джон Скит объяснил это очень хорошо здесь stackoverflow.com/questions/20868103/…
MuiBienCarlota
3
Спасибо за Tupleальтернативу. Очень полезно.
Люк Во
19
это ужасно Tuple. : P
tofutim
36
Я думаю, что Named Tuples в C # 7 будет идеальным решением для этого.
Орад
3
@orad Мне особенно нравится это: частное асинхронное задание <(успешное выполнение bool, задание Job, строковое сообщение)> TryGetJobAsync (...)
J. Andrew Laughlin
51

Вы не можете иметь 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;
}

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

Alex
источник
2
Я предпочитаю это решение вместо использования Tuple. Более чистый!
MiBol
31

Решением 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;
jv_
источник
12

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

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.

Скотт Тернер
источник
9

Приятной особенностью 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.");
    }
}
Бинки
источник
6

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

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

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

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

Это больше похоже на 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);
    }
}

Подход 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;
    }
}

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

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

Удачи.

Джерри Никсон
источник
1
Что касается третьего подхода, вы уверены, что цепочка ContinueWithвызовов имеет ожидаемый результат? Насколько я понимаю, второй ContinueWithпроверит успешность первого продолжения, а не исходную задачу.
Теодор Зулиас
1
Приветствия @ TheodorZoulias, это острый глаз. Исправлена.
Джерри Никсон
1
Отбрасывание исключений для управления потоком - это огромный запах кода для меня - это повысит вашу производительность.
Ян Кемп
Нет, @IanKemp, это довольно старая концепция. Компилятор эволюционировал.
Джерри Никсон
4

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

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

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

Поэтому я пришел к следующему решению:

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

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

     public async Task<AsyncOut<bool, string>> TryReceiveAsync()
     {
         string message;
         bool success;
         // ...
         return (success, message);
     }
  3. Вызовите асинхронный Try-метод следующим образом:

     if ((await TryReceiveAsync()).Out(out string msg))
     {
         // use msg
     }

Для параметров множественного выхода вы можете определить дополнительные структуры (например, AsyncOut <T, OUT1, OUT2>) или вы можете вернуть кортеж.

Майкл Гелинг
источник
Это очень умное решение!
Теодор Зулиас
2

Ограничение 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);
}

Использование метода TaskCompletionSourceand 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параметр никогда не будет завершен.

Теодор Зулиас
источник
1
Это очень хорошее решение для некоторых случаев.
Джерри Никсон
1

Я думаю, что использование 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):
}
Пол Марангони
источник
Вам не нужен NuGet, если вы используете .net-4.7 или netstandard-2.0.
Бинки
Эй, ты прав! Я только что удалил пакет NuGet, и он все еще работает. Спасибо!
Пол Марангони
1

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

Jpsy
источник
-2

Вы можете сделать это с помощью 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
Payam Buroumand
источник
Никогда не используйте .Результат. Это анти-паттерн. Спасибо!
Бен