programing tip

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

itbloger 2020. 6. 17. 19:00
반응형

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


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

행운을 빌어 요.


이와 같은 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):
}

다음은 명명 된 튜플과 튜플 해체로 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/을 참조하십시오.


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

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

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

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

    if ((await TryReceiveAsync()).Result(out T msg))
    {
        // use msg
    }
    

If you need multiple out parameters, of course you can define extra structs, like following:

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

The limitation of the async methods not accepting out parameters applies only to the compiler-generated async methods, these declared with the async keyword. It doesn't apply to hand-crafted async methods. In other words it is possible to create Task returning methods accepting out parameters. For example lets say that we already have a ParseIntAsync method that throws, and we want to create a TryParseIntAsync that doesn't throw. We could implement it like this:

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

Using the TaskCompletionSource and the ContinueWith method is a bit awkward, but there is no other option since we can't use the convenient await keyword inside this method.

Usage example:

if (await TryParseIntAsync("-13", out var result))
{
    Console.WriteLine($"Result: {await result}");
}
else
{
    Console.WriteLine($"Parse failed");
}

Update: If the async logic is too complex to be expressed without await, then it could be encapsulated inside a nested asynchronous anonymous delegate. A TaskCompletionSource would still be needed for the out parameter. It is possible that the out parameter could be completed before the completion of the main task, as in the example bellow:

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

This example assumes the existence of three asynchronous methods GetResponseAsync, GetRawDataAsync and FilterDataAsync that are called in succession. The out parameter is completed on the completion of the second method. The GetDataAsync method could be used like this:

var data = await GetDataAsync("http://example.com", out var rawDataLength);
Console.WriteLine($"Data: {data}");
Console.WriteLine($"RawDataLength: {await rawDataLength}");

Awaiting the data before awaiting the rawDataLength is important in this simplified example, because in case of an exception the out parameter will never be completed.

참고URL : https://stackoverflow.com/questions/18716928/how-to-write-a-async-method-with-out-parameter

반응형