回答:
ref
またはout
パラメーター付きの非同期メソッドは使用できません。
これは、このMSDNのスレッドでは不可能である理由ルシアンWischikは説明する:http://social.msdn.microsoft.com/Forums/en-US/d2f48a52-e35a-4948-844d-828a1a6deb74/why-async-methods-cannot-have -ref-or-out-parameters
なぜ非同期メソッドが参照外パラメータをサポートしないのか?(またはrefパラメーター?)これはCLRの制限です。イテレータメソッドと同様の方法で非同期メソッドを実装することを選択しました。つまり、コンパイラーがメソッドをステートマシンオブジェクトに変換することによって。CLRには、「出力パラメーター」または「参照パラメーター」のアドレスをオブジェクトのフィールドとして保存する安全な方法がありません。参照外パラメーターをサポートする唯一の方法は、非同期機能が、コンパイラーの書き換えではなく、低レベルのCLRの書き換えによって行われた場合です。私たちはそのアプローチを検討しましたが、それは多くのことを成し遂げましたが、最終的には非常にコストがかかり、決して起こらなかったでしょう。
この状況の一般的な回避策は、代わりにasyncメソッドでタプルを返すことです。メソッドを次のように書き直すことができます。
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;
アレックスは読みやすさについて素晴らしいポイントを作りました。同様に、関数は、返される型を定義するのに十分なインターフェイスでもあり、意味のある変数名も取得できます。
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
、メソッドの結果がの場合に設定される「Try」メソッドに似ていtrue
ます。そうでなければ、あなたは気にしませんmyOp
。
out
パラメータの1つの優れた機能は、関数が例外をスローした場合でもデータを返すために使用できることです。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
準バージョンのメソッドについて検討できる3つのコアアプローチですTry
。
これは、パラメーター付きのの代わりにをTry
返すだけのsync メソッドのように見えますが、C#では許可されていません。tuple
bool
out
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
して2つの異なるパスにリダイレクトすることです。
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
呼び出しをチェーンすることで期待どおりの結果が得られることを確信していますか?私の理解によると、2番目ContinueWith
は、最初のタスクの成功ではなく、最初の継続の成功をチェックします。
基本的にasync-await-paradigmと互換性がないように見えるTry-method-patternを使用するのと同じ問題がありました...
私にとって重要なのは、1つのif節内でTryメソッドを呼び出すことができ、事前にout変数を事前定義する必要はありませんが、次の例のようにインラインで実行できることです。
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メソッドを定義します。
public async Task<AsyncOut<bool, string>> TryReceiveAsync()
{
string message;
bool success;
// ...
return (success, message);
}
次のように非同期のTryメソッドを呼び出します。
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
と、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);
}))();
}
この例では、3つの非同期メソッドが存在することを前提としGetResponseAsync
、GetRawDataAsync
かつFilterDataAsync
その連続して呼ばれています。out
パラメータは、第2の方法の完了時に終了します。この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