パラメータなしで非同期メソッドを記述する方法は?


176

次のoutように、パラメーターを指定して非同期メソッドを記述したいと思います。

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

これをどのように行うのGetDataTaskAsyncですか?

回答:


279

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

10
複雑すぎず、問題が多すぎる可能性があります。Jon Skeetがここで非常によく説明しましたstackoverflow.com/questions/20868103/…–
MuiBienCarlota

3
Tuple代替案をありがとう。非常に役立ちます。
Luke Vo

19
醜いTupleです。:P
tofutim 2016年

36
C#7の名前付きタプルは、このための完璧なソリューションになると思います。
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
私はタプルを使用する代わりにこのソリューションを好みます。もっときれいな!
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

アレックスは読みやすさについて素晴らしいポイントを作りました。同様に、関数は、返される型を定義するのに十分なインターフェイスでもあり、意味のある変数名も取得できます。

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


9

outパラメータの1つの優れた機能は、関数が例外をスローした場合でもデータを返すために使用できることです。asyncメソッドでこれを行うのに最も近い方法は、新しいオブジェクトを使用して、asyncメソッドと呼び出し元の両方が参照できるデータを保持することです。別の方法は、別の回答で提案されているようにデリゲート渡すことです。

これらの手法はいずれも、コンパイラーによる強制のようなものはありませんout。つまり、コンパイラでは、共有オブジェクトに値を設定したり、渡されたデリゲートを呼び出したりする必要はありません。

以下は、共有オブジェクトを使用して模倣しref、メソッドや他のさまざまなシナリオでout使用するためのasyncrefそして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.");
    }
}

6

Tryパターンが大好きです。きちんとしたパターンです。

if (double.TryParse(name, out var result))
{
    // handle success
}
else
{
    // handle error
}

しかし、それはに挑戦していasyncます。それは私たちが実際の選択肢がないという意味ではありません。以下は、パターンのasync準バージョンのメソッドについて検討できる3つのコアアプローチですTry

アプローチ1-構造を出力する

これは、パラメーター付きのの代わりにをTry返すだけのsync メソッドのように見えますが、C#では許可されていません。tupleboolout

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して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か月で明確な質問に答える必要がない次の開発者を思い出してください。あなたのコードは、開発者がこれまでに持つ唯一のドキュメントになることができます。

幸運を祈ります。


1
3番目のアプローチについて、ContinueWith呼び出しをチェーンすることで期待どおりの結果が得られることを確信していますか?私の理解によると、2番目ContinueWithは、最初のタスクの成功ではなく、最初の継続の成功をチェックします。
Theodor Zoulias

1
乾杯@TheodorZoulias、それは鋭い目です。修繕。
ジェリーニクソン、

1
フロー制御の例外をスローすると、コードの臭いが大きくなり、パフォーマンスが低下します。
イアンケンプ

いいえ、@ IanKemp、それはかなり古い概念です。コンパイラは進化しました。
ジェリーニクソン、

4

基本的にasync-await-paradigmと互換性がないように見えるTry-method-patternを使用するのと同じ問題がありました...

私にとって重要なのは、1つのif節内でTryメソッドを呼び出すことができ、事前にout変数を事前定義する必要はありませんが、次の例のようにインラインで実行できることです。

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メソッドを定義します。

     public async Task<AsyncOut<bool, string>> TryReceiveAsync()
     {
         string message;
         bool success;
         // ...
         return (success, message);
     }
  3. 次のように非同期のTryメソッドを呼び出します。

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

複数の出力パラメーターの場合、追加の構造体(AsyncOut <T、OUT1、OUT2>など)を定義するか、タプルを返すことができます。


これは非常に賢い解決策です!
Theodor Zoulias

2

パラメーターasyncを受け入れないメソッドの制限は、outコンパイラー生成の非同期メソッドにのみ適用され、これらはasyncキーワードで宣言されます。手作りの非同期メソッドには適用されません。つまりTaskoutパラメータを受け入れる戻りメソッドを作成することができます。たとえば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つの非同期メソッドが存在することを前提としGetResponseAsyncGetRawDataAsyncかつ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です。


1
これは、いくつかのケースで非常に優れたソリューションです。
ジェリーニクソン

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 2018年

ねえ、あなたは正しいです!その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.