WebClientよりHttpWebRequestを使った方がいい理由

先日のWindows Phone Arch@Nagoyaで質問をいただきました。

「なぜWebClientよりHttpWebRequestを使った方がよいのか?」と。

残念ながらその場では答えることができなかったのでここで回答させてもらいます。(まさかの質問でした!)

WebClientよりHttpWebRequest

WebClientはHttpWebRequestのラッパーなのでWebClientで行う処理はHttpWebRequestで置き換えることが可能です。

WebClientを利用するのは、HttpWebRequestだと冗長になりやすいBeginGetResponseとEndGetResponseの処理をすっきり書けること、そして受け取ったレスポンスの処理でわざわざUIスレッドに切り替える必要がないからだと思います。

便利なWebClientですがひとつ知っておくべき挙動があります。WebClientのDownloadStringCompletedのイベントがコールされるのはWebClientがnewされたスレッドだということです。多くの場合、WebClientはUIスレッドで生成するのでDownloadStringCompletedのイベントもUIスレッドで呼ばれます。だからWebClientを利用するとUIスレッドに切り替える処理が必要がないのです。

そのため、DownloadStringCompletedで呼ばれるメソッド内で負荷の掛かる処理を行うと、ダイレクトにUIスレッドに影響を与えることになってしまいます。

var client = new WebClient();  // UIスレッドでインスタンスを生成
client.DownloadStringCompleted += (_, __) =>
{
    // ここはUIスレッド
    // ここで負荷のかかる処理を行うとUIスレッドに影響を与える
};
client.DownloadStringAsync(new Uri(url));

その問題を回避するためには、HttpWebRequestを利用してダウンロードしたデータをすべて表示用に加工し終えてからUIスレッドにデータを渡すようにします。つまりデータを加工する処理が終わるまでは極力UIスレッドで処理を行わないようにするわけです。

var request = WebRequest.Create(new Uri(url)) as HttpWebRequest;
request.BeginGetResponse((ar) =>
{
    // ここはUIスレッドとは別スレッド
    var response = request.EndGetResponse(ar);
    // ここでデータを表示用に加工する

    Dispatcher.BeginInvoke(() =>
    {
        // ここはUIスレッド
        // ここではデータをビューに渡すだけ
    });
},
null);

もしWebClientのDownloadStringCompletedでダウンロードしたデータを加工している場合は、WebClientからHttpWebRequestに変更し、データを加工してからUIスレッドに切り替えるとUIがスムーズになる可能性があります。

サンプル

以下に簡単な例を示します。
ここではレスポンスを受け取る処理で負荷をかけた場合、UIスレッドにどのような影響があるのかを調べるサンプルです。
プログレスバーとWebClientボタンとHttpWebRequestボタンの3つを配置します。プログレスバーを配置するのは、UIスレッドに負荷がかかるとプログレスバーが止まるので目視でUIスレッドへの負荷を確認できるためです。

MainPage.xaml

<Grid x:Name="LayoutRoot" Background="Transparent">
  <Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>
     <RowDefinition Height="*"/>
  </Grid.RowDefinitions>

  <!--TitlePanel は、アプリケーション名とページ タイトルを格納します-->
  <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
    <TextBlock
       x:Name="ApplicationTitle"
       Text="WebClient vs HttpWebRequest"
       Style="{StaticResource PhoneTextNormalStyle}"/>
  </StackPanel>

  <!--ContentPanel - 追加コンテンツをここに入力します-->
  <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
      <!-- プログレスバー(負荷の目視のため) -->
      <ProgressBar x:Name="progress1" IsIndeterminate="False"></ProgressBar>
      <!-- WebClientボタン -->
      <!-- このボタンを押すとWebClientを利用してデータをダウンロードします -->
      <Button
        Name="webClientButton"
        Content="WebClient"
        Height="72"
        HorizontalAlignment="Left"
        Margin="0,125,0,0"
        VerticalAlignment="Top"
        Width="450"
        Click="webClientButton_Click" />
      <!-- HttpWebRequestボタン -->
      <!-- このボタンを押すとHttpWebRequestを利用してデータをダウンロードします -->
      <Button
        Name="httpWebRequestButton"
        Content="HttpWebRequest"
        Height="72"
        HorizontalAlignment="Left"
        Margin="0,203,0,0"
        VerticalAlignment="Top"
        Width="450"
        Click="httpWebRequestButton_Click" />
  </Grid>
</Grid>

各ボタンを押すとプログレスバーが進みだし、WebClientの処理、HttpWebRequestの処理が行われます。そしてレスポンスを受け取る処理で負荷を掛けたときにUIスレッドで動くプログレスバーがどんな動きをするのかを確認します。
MainPage.xaml.cs

private void webClientButton_Click(object sender, RoutedEventArgs e)
{
    progress1.IsIndeterminate = true;

    var client = new WebClient();
    client.DownloadStringCompleted += (_, __) =>
    {
        // ここはUIスレッド
        Thread.Sleep(3000);   // 負荷をかける(スレッドを止める)
        progress1.IsIndeterminate = false;
    };
    client.DownloadStringAsync(
        new Uri("https://api.twitter.com/1/statuses/public_timeline.json"));
}

private void httpWebRequestButton_Click(object sender, RoutedEventArgs e)
{
    progress1.IsIndeterminate = true;

    var request = WebRequest.Create(
        new Uri("https://api.twitter.com/1/statuses/public_timeline.json")) as HttpWebRequest;
    request.BeginGetResponse((ar) =>
    {
        // ここはUIスレッドとは別スレッド
        var response = request.EndGetResponse(ar);
        Thread.Sleep(3000);    // 負荷をかける(スレッドを止める)

        Dispatcher.BeginInvoke(() =>
        {
            // ここはUIスレッド
            progress1.IsIndeterminate = false;
        });
    },
    null);
}

結果はWebClientの方はSleepしている間プログレスバーが止まってしまいますが、HttpWebRequestの方はSleepのときも普通にプログレスバーが動いています。
つまりWebClientでレスポンスを受け取る処理では極力負荷がかからないように注意した方がよいです。