連載:C# 3.0入門

第7回 LINQ応用編

株式会社ピーデー 川俣 晶
2008/10/10
Page1 Page2 Page3

LINQの難しさ

 個人的に、C#プログラミングでのLINQ使用量は増える一方である。例えば、いま書いているプログラムの中で、クエリ式を含む最長の行を探してみたところ、以下の行がそれであった(オブジェクトの初期化の一部)。

Is可視 = (m) => General.IsMission達成( Missions.初めての企画成功 ) && (from n in Items.GetItemListIncludeItemNull() where State.GetItemCount(n) > 0 && t.IsTargetItem(n) select n).Any(), ……

 余談だが、この行は冗長である。実際は以下のように書けば十分である。

Is可視 = (m) => General.IsMission達成( Missions.初めての企画成功 ) && Items.GetItemListIncludeItemNull().Any( State.GetItemCount(n) > 0 && t.IsTargetItem(n) );

 それにもかかわらずLINQのクエリ式を使ったのは、引数のないAnyメソッドの使い勝手がなかなかよいことに気付き、使ってみたかったからである(この話題は詳しく後述する)。

 ところで、クエリ式は最低でも「from」「in」「select」などの3つ程度のキーワードを含む2つの句を必要とし、実用上はこれにwhere句を足した「4キーワード3句」程度が最低の長さとなる。つまり、式としては長くなりがちである。さらに、カッコでくくって「.Any()」や、「.Count()」などを付けたり、クエリ式を丸ごとforeach文に入れたりしてしまうことも珍しくなくなる。こうなると、行の長さは増す一方だ。

 だが、そこで発生する最大の問題は、実は行の長さにはない。いくら行が長くなっても、適当なところで改行を入れることができるからだ。問題は、そこにはない。

 長いクエリ式はデバッグの際に不便を生じることが多い、と感じるのである。いや、短いクエリ式でも問題が出るときには出てしまう。一例を以下に紹介しよう。

 以下のプログラムは1から4までの整数値に対して、偶数か奇数かを判定するが、「odd」と書くべきところに誤って「null」と書いているため、例外で落ちてしまう。これを、従来型のコーディングと、クエリ式を使ったコーディングで比較してみる。

using System;

class Program
{
  static void Main(string[] args)
  {
    int[] t = { 1, 2, 3, 4 };

    foreach (var n in t)
    {
      if (n >= 2)
      {
        string s = n % 2 == 0 ? "even" : null;
        Console.WriteLine(s.ToUpper());
      }
    }
  }
}
リスト1 従来型のコーディング例

using System;
using System.Linq;

class Program
{
  static void Main(string[] args)
  {
    int[] t = { 1, 2, 3, 4 };

    foreach (var m in
                  from n in t
                  where n >= 2
                  select n % 2 == 0 ? "even" : null)
    {
      Console.WriteLine(m.ToUpper());
    }
  }
}
リスト2 クエリ式を使ったコーディング例

 さて、これをVisual Studioでデバッグ実行しても、当然例外でプログラムの実行は止まる。このとき知りたいのは、配列tの値のうち、どの値を処理しているときに例外が起きたかだ。

 従来型のコーディング例(リスト1)では、変数nにそれが入っている。変数nの値を調べれば、それが「3」だと分かる。その結果、「3 % 2」は0ではないので、このときnullが選択されていることが判明し、バグの原因に到達できる。

 ところが、クエリ式を使ったコーディング例(リスト2)では同じことができない。例外で止まったタイミングで知ることができるのは、配列tと変数mの値だけで、配列tの要素の値を格納した変数nの値は知ることができない。これはあくまでクエリ式内でのみ有効な変数であり、クエリ式外で停止した状態では、スコープ外なのである。この例では、変数mの値がnullなので、そこから容易にバグの原因を推定できてしまうが、実際のコードはもっと複雑で分かりにくい。

 なぜこのような問題が起きるのだろうか。

 それはクエリ式がそれ単体で1つの世界を構成しているからだ。クエリ式の外部から見れば、クエリ式は1つのブラックボックスとなる。これに対して、foreach文を用いて自前で処理する場合は、foreach文は外部世界の一部となり、ホワイトボックスとして機能するので、デバッグ時に内外の状況を調べやすい。

 ではクエリ式とはデバッグしにくい難物であり、使わない方がよいのだろうか? そうではない。

 なぜなら実際に書かれたコードの機能性が違うからだ。クエリ式を使ったコーディング例とほぼ等価のコードをクエリ式抜きで記述すると以下のようになる。

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
  class SampleIterator
  {
    public int[] T;

    public IEnumerator<string> GetEnumerator()
    {
      foreach (var n in T)
      {
        if (n >= 2) yield return n % 2 == 0 ? "even" : null;
      }
    }
  }

  static void Main(string[] args)
  {
    int[] t = { 1, 2, 3, 4 };

    foreach (var m in new SampleIterator() { T = t })
    {
      Console.WriteLine(m.ToUpper());
    }
  }
}
リスト3 リスト2をクエリ式抜きで記述

 このコードを見れば、Console.WriteLineで停止したとき、変数nの値が確認できないことは当然だと分かるだろう。そして、Console.WriteLineの行から変数nがアクセスできないのは、極めて合理的な仕様である。

 Mainメソッドを書いているプログラマは、実際に列挙を行うコードの詳細など知りたくはないのだ。それはすべてうまく稼働するという前提で、Main関数の記述に集中したいのだ。見えないことで困るのは、うまく稼働しなかった場合に、値のチェックが不便になることだけである。

 つまり、クエリ式を使うとデバッグが難しくなるというのは誤解であり、実際には従来と何ら変わりはないのである。ただ、クエリ式を使うと局所的な1つの式に多くの機能を詰め込めるために、「直感的に見えそうだと思ってしまう変数」が出現してしまうだけの話である。

クエリ式のデバッグ・テクニック

 ここでクエリ式をデバッグする際のヒントを簡単に説明しておこう。

 まず大前提として、1つのクエリ式にあらゆる処理を詰め込むことはやめよう。それは問題の切り分けの手間を増大させるほか、ソース・コードのメンテナンス性も落とすことになり、よい選択とはいえない。

 次に、クエリ式の結果とする「型」についてだ。以下は名前と点数のペアのデータから、点数を抜き出して出力する例である。

using System;
using System.Linq;

class Program
{
  class Pair
  {
    public string Name;
    public int Point;
  }

  static void Main(string[] args)
  {
    Pair[] t =
    {
       new Pair() { Name = "太郎", Point = 100 },
       new Pair() { Name = "花子", Point = 100 },
    };

    foreach (var m in
                  from n in t
                  select n.Point)
    {
      Console.WriteLine(m);
    }
    // 出力:
    // 100
    // 100
  }
}
リスト4 結果だけを得るクエリ式

 このプログラムでは、出力される点数がおかしいと気付いたとき、Console.WriteLineの行でブレークさせて調べても、点数が分かるだけで誰の点数かは分からない。

 そこで、以下のように点数ではなくPairクラスを結果の型としてみる。

foreach (var m in
              from n in t
              select n)
{
  Console.WriteLine(m.Point);
}
リスト5 結果の型を変える

 すると、Console.WriteLineの行でブレークさせたとき、点数だけでなく名前も知ることができる。

 少なくとも内蔵オブジェクトに対するクエリを行う“LINQ to Objects”である限り、結果が整数でも参照でも負荷の重さは大差ないだろう。これは使ってよいテクニックだと思う(ただし、常によいかは分からない。よくないケースも多いだろう)。

 さて、実際にバグが発生して調べる場合は、トレース実行よりもブレークポイントが役に立つ。列挙と絡むため、個々の式が評価される回数が多く、[F11]キーを繰り返し押すだけではなかなか実行が進まないからである。

 その際、ブレークポイントを仕掛けられる場所がどこか、正確に把握しておくとよい。例えば、以下の式を題材に見てみよう。

from n in t
where n >= 2
select n % 2 == 0 ? "even" : null

 この場合、ブレークポイントが設定可能な場所は以下の3カ所だ。

クエリ式全体
「n >= 2」部分
「n % 2 == 0 ? "even" : null」部分

 ブレークポイントは、 のいずれかの場所にキャレットを合わせて[F9]キーで設定できる。クエリ式全体に設定するときだけは 以外の場所で[F9]キーを押す。

 そして、ブレークポイントに設定できるブレーク条件の条件式も活用する必要がある。クエリ式は、クエリ式内部の式が計算される前後がブラックボックスであるため、その式に条件を付けて動作を捕まえるしかない。

 例えば、上記の例でいえば、 がnullになるタイミングで止めるには、この式にブレークポイントを付け、コード行の左側にあるブレークポイントの赤丸を右クリックして[条件]を選び、以下の画面のように条件式を指定する。


条件付きのブレークポイントをLINQの条件部分に設定
左端にある赤丸を右クリックして[条件]を選択し、条件式として「null == (n % 2 == 0 ? "even" : null)」を指定する。

 これで、この式がnullになった瞬間、ここでプログラムはブレークする。そして、変数nの値を調べることができる。もちろん、直感的に式の意味を把握できるなら、もっと簡潔な条件式を書いてもよい(「n % 2 != 0」と書けば十分である)。

 

 INDEX
  C# 3.0入門
  第7回 LINQ応用編
  1.LINQの難しさ/クエリ式のデバッグ・テクニック
    2.join句のグループ化結合/左外部結合/DefaultIfEmptyメソッド/from句の2重使用
    3.let句/クエリのインスタンス化/クエリ結果の個数/Anyメソッドと存在チェック
 
インデックス・ページヘ  「C# 3.0入門」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間