답변:
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);
}
Tuple
대안 주셔서 감사합니다 . 매우 도움이됩니다.
Tuple
입니다. : P
이미 언급했듯이 메소드 에는 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
. 즉, 컴파일러는 공유 객체에 값을 설정하거나 전달 된 델리게이트를 호출 할 필요가 없습니다.
여기 모방에 공유 객체를 사용하여 구현 한 예이다 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.");
}
}
나는 Try
패턴을 좋아한다 . 깔끔한 패턴입니다.
if (double.TryParse(name, out var result))
{
// handle success
}
else
{
// handle error
}
그러나로 도전합니다 async
. 그렇다고 실제 옵션이 없다는 의미는 아닙니다. 다음은 패턴 async
의 준 버전에서 메소드에 대해 고려할 수있는 세 가지 핵심 접근법 Try
입니다.
대부분의 동기화의 등이 보이는 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);
}
}
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 # 컴파일러는 너무나 똑똑하여 거의 확실 하게이 옵션을 선택하는 것이 안전하다고 생각합니다.
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 개월 만에 다음 질문에 답하지 않아도되는 다음 개발자를 기억하십시오. 코드는 개발자가 가질 수있는 유일한 문서 일 수 있습니다.
행운을 빕니다.
ContinueWith
통화가 예상되는 결과를 얻습니까? 내 이해에 따르면 두 번째 ContinueWith
는 원래 작업의 성공이 아니라 첫 번째 연속의 성공을 확인할 것입니다.
Try-method-pattern을 사용하는 것과 같은 문제가 있었는데 기본적으로 async-await-paradigm과 호환되지 않는 것 같습니다 ...
나에게 중요한 것은 단일 if-clause 내에서 Try-method를 호출 할 수 있고 이전에 변수를 미리 정의 할 필요는 없지만 다음 예제와 같이 인라인으로 수행 할 수 있다는 것입니다.
if (TryReceive(out string msg))
{
// use msg
}
그래서 다음 해결책을 생각해 냈습니다.
도우미 구조체를 정의하십시오.
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);
}
다음과 같이 비동기 Try-method를 정의하십시오.
public async Task<AsyncOut<bool, string>> TryReceiveAsync()
{
string message;
bool success;
// ...
return (success, message);
}
다음과 같이 비동기 Try-method를 호출하십시오.
if ((await TryReceiveAsync()).Out(out string msg))
{
// use msg
}
다중 출력 파라미터의 경우 추가 구조체를 정의하거나 (예 : AsyncOut <T, OUT1, OUT2>) 튜플을 반환 할 수 있습니다.
매개 변수를 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
and 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
합니다.
이와 같은 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/
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