質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

89.71%

[C#]非同期通信処理とそのコールバック方法について

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 1
  • VIEW 5,043

yyokii

score 33

 前提

・WPFアプリ
・Visual Studio
・C#

 お聞きしたいこと

API通信処理を記述しているのですが、
・非同期通信とその際のコールバック実装てこれでいいの?
と悩んでおり、アドバイス等頂ければ幸いです。

→ 実装済みで期待通りの動作はしています。
ただ、C#の経験が浅くまたデスクトップアプリを作成するのが初めてなので、自分以外の人が見て、このコードだと潜在的に○○な問題を孕んでいる、とかここはこういう書き方したほうが効率的だよ、といった意見があればお聞きしたいです。

 コード

以下がPostの非同期通信処理メソッド、ログインAPIを叩くメソッドのコードです。
両方ともAPIManagerという通信処理関連のメソッドをまとめたクラスにて記述しており、ここのメソッドを他のクラスで使用する想定です。

↓Post通信を非同期で行うメソッドです。
・通信の汎用メソッドとして使用
・成功したらJTokenを返す
・通信エラーなら"error"という文字列を返す
・レスポンスが200以外なら"fail"とい文字列を返す

public static async Task<JToken> RequestPostAsync(string url, Dictionary<string, object> contentData)
        {
            var contentJson = Newtonsoft.Json.JsonConvert.SerializeObject(contentData);
            HttpContent content = new StringContent(contentJson);
            content.Headers.Add("hoge", "hoge");
            content.Headers.Add("fuga", "fuga");

            // 非同期処理実行
            using (HttpClient client = new HttpClient(new WebRequestHandler
            {
                CachePolicy = new System.Net.Cache.HttpRequestCachePolicy(System.Net.Cache.HttpRequestCacheLevel.NoCacheNoStore)
            }))
            {
                client.Timeout = TimeSpan.FromMilliseconds(10000);

                HttpResponseMessage response;
                try
                {
                    response = await client.PostAsync(url, content);
                }
                catch (HttpRequestException)
                {
                    System.Diagnostics.Debug.WriteLine("通信エラー");
                    return Util.Define.ERROR;
                }

                if (response.IsSuccessStatusCode)
                {
                    string result = await response.Content.ReadAsStringAsync();
                    if (result != "")
                    {
                        JToken jToken = JToken.Parse(result);
                        return jToken;
                    }
                    else
                    {
                        // レスポンスで受け取る情報なし
                        return "";
                    }
                }
                else
                {
                    System.Diagnostics.Debug.WriteLine("通信失敗(fail) statusCode:" + response.StatusCode);
                    return Util.Define.FAIL;
                }
            }

↓上記メソッドを用いて例えばログインAPIを以下のように実装しています
・引数にて通信終了後に実行するメソッドを受け取る
→ 例えば他のクラスでこのメソッドを使用し、通信成功時にTextBlockのTextを変更するメソッドを引数としてわたしていたら循環参照起きる??(weakの設定が必要かなと思っています)

public static async void LoginAsync(string name, string password, Action successLogin, Action failLogin, Action errorLogin)
        {
            var content = new Dictionary<string, Object>()
            {
                {"name:", name},
                {"pass", password}
            };

            var response = await RequestPostAsync("loginApiPass", content, "");
            if (response.ToString() == Util.Define.ERROR)
            {
                errorLogin();
            }
            else if (response.ToString() == Util.Define.FAIL)
            {
                failLogin();
            }
            else
            {
                successLogin();
            }
        }
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • Zuishin

    2018/03/30 17:00

    テストしてみたらいいのでは?

    キャンセル

  • yyokii

    2018/03/30 18:12

    情報が不足しておりすみません。 実行して非同期通信、コールバックが動いているのは確認できています。質問の意図としては、「(私はC#の経験が浅く、またデスクトップアプリを作成するのが初めてなので、)自分以外の人が見て、このコードだと(動くけども)潜在的に○○な問題を孕んでいる、とかここはこういう書き方したほうが効率的だよ、といった意見があれば聞きたい」ということでした。質問文の不備ですので修正させていただきます。

    キャンセル

回答 1

checkベストアンサー

+3

こんにちは。

今どきのC#で非同期を書くなら、そもそも明示的にコールバックを使うことがないです。
何故なら、async/awaitとTaskが高度に抽象化されたコールバックの連鎖そのものだからです。
現状のLoginAsyncは非同期処理を投げっぱなし(async void)なので、これをasync Task<LoginResult>に書き換え、呼び出し元でawaitして分岐させます。

// LoginResultみたいなenumとかが定義されてると仮定する
public static async Task<LoginResult> LoginAsync(string name, string password)
{
    //
    // なんかの非同期処理をした後、
    //

    if (response.ToString() == Util.Define.ERROR)
    {
        return LoginResult.Error; // 普通に結果をreturnする
    }
    else if (response.ToString() == Util.Define.FAIL)
    {
        return LoginResult.Fail;
    }
    else if ...
}

// 呼び出し元
{
    var result = await LoginAsync("name", "****"); // awaitして変数に代入

    if (result == ...) // resultを見て次の処理を書く
    {
        ...
    }
}

このように、asyncで定義したメソッドが結果を返すようにして、通常の同期メソッドと同じように、一連の流れを意識しつつ書くだけでいいのです。


循環参照を気にしておられるようですが、具体的にどのインスタンスがどのインスタンスと循環している(あるいは、しそう)だと思ったのでしょうか?
循環参照というのは、特定のインスタンス同士がお互いの参照をフィールドに保持している状態のことで、メソッド内での参照(スタック領域に確保される参照)は循環にはなりえません。
また、C#のGCは参照カウント方式ではないので、仮にインスタンス同士が循環参照していたとしても、それら全てのインスタンスへのルートからの参照が消滅した時点でGCの対象となるため、例えばstatic領域にListを置いて何かのインスタンスを詰め続けるみたいな危ういコーディングをしない限りは問題が起こることはありません。


その他、非同期に関する細かいことをちょっと書いておきました。

 async voidについて

async voidは基本的には使用禁止です。
戻り値を返す場合はTask<T>を、同期メソッドにおける戻り値voidを表すにはTaskを返すようにしてください。
何故なら、async voidを書くとその実行結果をハンドルすることができない(まさに質問のコードのように、コールバックを使ってハンドルする方法しかない)ためです。
Taskを返すようにすることで、その自前の処理をasync/awaitによる非同期フローに組み込むことができるため、これを使わない理由はないです。
async voidを使ってよいのは、UIのイベントハンドラ(Button等)に非同期メソッドを使用する場面だけです。

 using (HttpClient)

HttpClientは実はusingで使うと困った問題を引き起こす設計上の欠陥が存在します(初心者殺し……)。
具体的には、何回もHttpClientの生成と破棄を繰り返すとソケットを食い潰します。
RequestPostAsyncメソッドはまさに再利用を目的としたメソッド切り出しであるため、この場合HttpClientはstaticフィールドに確保し、usingを外して再利用したほうが良いです(ちゃんとしたインスタンス管理用のホルダを作るとなお良い)。

 TaskのWait()のこと

async/awaitを用いた非同期メソッドは、処理自体は非同期で実行されるが、「同階層の処理は全て同じスレッド上で実行しようとする」という設計になっています。
この仕様に起因するもので、非同期メソッドの設計自体に問題がなくても、その戻り値TaskをWait()しようとすると途端に致命的な問題が発生します。
デッドロックです。
WaitメソッドとResultプロパティの呼び出しは非常に危険なので、徹底的に使用を控えるようにしてください。
また、余裕があれば、ConfigureAwait(false)について情報収集を行っておくことをオススメします。


長くなっちゃったのでまとめます。

  • コールバックなんて使わなくて、Taskでasync/awaitすればいい
  • 循環参照はstatic変数とかに入れてなければ大体問題ない
  • async voidは使わない
  • HttpClientはusing非推奨という罠がある
  • Wait()とResultはヤバイ

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 89.71%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる