매개 변수가없는 비동기 메서드를 작성하는 방법은 무엇입니까?


176

다음과 같이 out매개 변수를 사용 하여 비동기 메서드를 작성하고 싶습니다.

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

이 작업을 어떻게 수행 GetDataTaskAsync합니까?

답변:


279

ref또는 out매개 변수를 사용하여 비동기 메소드를 가질 수 없습니다 .

Lucian Wischik은이 MSDN 스레드에서 이것이 불가능한 이유를 설명합니다 . Ref-or-out-parameters

비동기 메소드가 참조 기준 매개 변수를 지원하지 않는 이유는 무엇입니까? (또는 ref 매개 변수?) 이것이 CLR의 한계입니다. 우리는 반복기 메소드와 유사한 방식으로 비동기 메소드를 구현하기로 결정했습니다. 즉, 컴파일러를 통해 메소드를 상태 머신 객체로 변환하는 것입니다. CLR은 "out parameter"또는 "reference parameter"의 주소를 객체의 필드로 저장하는 안전한 방법이 없습니다. 외부 참조 매개 변수를 지원하는 유일한 방법은 비동기 기능이 컴파일러 재 작성 대신 저수준 CLR 재 작성에 의해 수행 된 경우입니다. 우리는 그 접근 방식을 살펴 보았고, 많은 노력을 기울 였지만, 결국에는 결코 일어나지 않을 정도로 비용이 많이 들었을 것입니다.

이 상황에 대한 일반적인 해결 방법은 async 메서드가 대신 Tuple을 반환하도록하는 것입니다. 다음과 같이 메소드를 다시 작성할 수 있습니다.

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대안 주셔서 감사합니다 . 매우 도움이됩니다.
Luke Vo

19
그것은 못생긴 것 Tuple입니다. : P
tofutim

36
C # 7의 Named Tuples 가 완벽한 솔루션 이라고 생각 합니다.
orad

3
@ orad 나는 특히 이것을 좋아한다 : private async Task <(bool success, Job job, string message)> 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
대신 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;

12

Alex는 가독성에 대해 큰 지적을했습니다. 마찬가지로 함수는 반환되는 유형을 정의하기에 충분한 인터페이스이며 의미있는 변수 이름도 얻습니다.

delegate void OpDelegate(int op);
Task<bool> GetDataTaskAsync(OpDelegate callback)
{
    bool canGetData = true;
    if (canGetData) callback(5);
    return Task.FromResult(canGetData);
}

호출자는 람다 (또는 명명 된 함수)를 제공하고 대리자로부터 변수 이름을 복사하여 인텔리전스 지원을 제공합니다.

int myOp;
bool result = await GetDataTaskAsync(op => myOp = op);

이 특정 방법은 myOp방법 결과가 인 경우 설정 되는 "시도"방법과 같습니다 true. 그렇지 않으면에 관심이 없습니다 myOp.


9

out매개 변수의 좋은 기능 중 하나 는 함수에서 예외가 발생하더라도 데이터를 반환하는 데 사용할 수 있다는 것입니다. async메서드를 사용 하여이 작업을 수행하는 것과 가장 가까운 것은 새 개체를 사용하여 async메서드와 호출자가 참조 할 수 있는 데이터를 보유하는 것입니다. 다른 방법은 다른 답변에서 제안한대로 대리인전달하는 것입니다. 입니다.

이 기술들 중 어느 것도 가지고있는 컴파일러로부터 어떠한 종류의 시행도하지 않을 것입니다 out. 즉, 컴파일러는 공유 객체에 값을 설정하거나 전달 된 델리게이트를 호출 할 필요가 없습니다.

여기 모방에 공유 객체를 사용하여 구현 한 예이다 refout함께 사용 async방법 및 기타 다양한 시나리오 refout사용할 수 없습니다가 :

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대신의를 boolout모든 노하우가 C #으로 허용되지 않습니다 우리 매개 변수.

var result = await DoAsync(name);
if (result.Success)
{
    // handle success
}
else
{
    // handle error
}

반환하는 방법으로 truefalse결코이 발생합니다 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는 원래 작업의 성공이 아니라 첫 번째 연속의 성공을 확인할 것입니다.
Theodor Zoulias

1
@TheodorZoulias를 건배, 그것은 날카로운 눈입니다. 결정된.
Jerry Nixon 2

1
흐름 제어에 대한 예외를 던지는 것은 큰 코드 냄새입니다. 성능이 저하 될 것입니다.
Ian Kemp

아니요, @IanKemp, 그것은 꽤 오래된 개념입니다. 컴파일러가 진화했습니다.
Jerry Nixon

4

Try-method-pattern을 사용하는 것과 같은 문제가 있었는데 기본적으로 async-await-paradigm과 호환되지 않는 것 같습니다 ...

나에게 중요한 것은 단일 if-clause 내에서 Try-method를 호출 할 수 있고 이전에 변수를 미리 정의 할 필요는 없지만 다음 예제와 같이 인라인으로 수행 할 수 있다는 것입니다.

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-method를 정의하십시오.

     public async Task<AsyncOut<bool, string>> TryReceiveAsync()
     {
         string message;
         bool success;
         // ...
         return (success, message);
     }
  3. 다음과 같이 비동기 Try-method를 호출하십시오.

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

다중 출력 파라미터의 경우 추가 구조체를 정의하거나 (예 : AsyncOut <T, OUT1, OUT2>) 튜플을 반환 할 수 있습니다.


이것은 매우 영리한 솔루션입니다!
Theodor Zoulias

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에는 여전히 A 가 필요합니다 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
이것은 어떤 경우에는 아주 좋은 해결책입니다.
Jerry Nixon

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

.net-4.7 또는 netstandard-2.0을 사용하는 경우 NuGet이 필요하지 않습니다.
binki

이봐, 네 말이 맞아! 방금 NuGet 패키지를 제거했는데 여전히 작동합니다. 감사!
Paul Marangoni

1

다음은 명명 된 튜플과 튜플 해체로 C # 7.0에서 수정 된 @dcastro의 답변 코드입니다.

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

await 키워드를 직접 사용하는 대신 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를 사용하지 마십시오. 안티 패턴입니다. 감사!
Ben
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.