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

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

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

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



답변

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


답변

이미 언급했듯이 메소드 에는 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;
}

코드를 더 쉽게 재사용 할 수 있으며 변수 나 튜플보다 더 읽기 쉽습니다.


답변

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;


답변

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.


답변

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


답변

나는 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 개월 만에 다음 질문에 답하지 않아도되는 다음 개발자를 기억하십시오. 코드는 개발자가 가질 수있는 유일한 문서 일 수 있습니다.

행운을 빕니다.


답변

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>) 튜플을 반환 할 수 있습니다.