Як написати метод асинхронізації з параметром out?


176

Я хочу написати метод асинхронізації з таким outпараметром:

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

Як це зробити GetDataTaskAsync?

Відповіді:


279

Ви не можете мати методи асинхронізації з параметрами 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);
}

10
Це не може бути занадто складним, що може спричинити занадто багато проблем. Джон Скит пояснив це дуже добре тут stackoverflow.com/questions/20868103 / ...
MuiBienCarlota

3
Дякую за Tupleальтернативу. Дуже корисний.
Лука Во

19
це некрасиво мати Tuple. : P
tofutim

36
Я думаю, що названі кортежі в C # 7 стануть ідеальним рішенням для цього.
орад

3
@orad Мені особливо подобається: приватне завдання async <(успіх bool, робота в роботі, повідомлення в рядку)> 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;
}

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


2
Я вважаю за краще це рішення, а не використання кортежу. Більш чисто!
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;

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);

Цей конкретний підхід схожий на метод "Спробувати", який 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метод синхронізації лише повернення 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 ...

Для мене важливо те, що я можу викликати Try-метод у межах одного if-clause і не потрібно заздалегідь визначати вихідні змінні раніше, але можу це робити в рядку, як у наступному прикладі:

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. Визначте асинхронний метод спробу:

     public async Task<AsyncOut<bool, string>> TryReceiveAsync()
     {
         string message;
         bool success;
         // ...
         return (success, message);
     }
  3. Виклик методу асинхронізації таким чином:

     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);
}

Використання методу 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параметр ніколи не буде завершено.


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/


-2

Для цього можна скористатися 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

Ніколи не використовуйте .Result. Це анти-шаблон. Дякую!
Бен
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.