連載:C# 3.0入門

第8回 LINQメソッド形式編

株式会社ピーデー 川俣 晶
2008/11/07
Page1 Page2 Page3 Page4

予約語のエスケープ

 まず軽いが重要な話題から入っていこう。

 クエリ式によくあるワナとして、クエリ式の予約語と使用するキーワードが衝突するという問題がある。

 例えば、これといって特にひねったコーディングは行っていない以下のリスト1を見ていただきたい。移動経路クラスのfromとtoはpublicなフィールドだから、FromとToにすべきだよな、といった感想は出るかもしれないが、それは名前付けルールの問題であって、コンパイルを通らないような問題ではない。

 しかし、このコードはコンパイルできない。

using System;
using System.Linq;

class 移動経路
{
  public string from;
  public string to;
}

class Program
{
  static void Main(string[] args)
  {
    移動経路[] 移動経路データ =
    {
      new 移動経路() { from = "新宿", to = "京王八王子"},
      new 移動経路() { from = "新宿", to = "橋本"},
      new 移動経路() { from = "渋谷", to = "吉祥寺"},
    };

    foreach (var 経路 in
          from n in 移動経路データ where n.from == "新宿" select n)
    {
      Console.WriteLine("{0}→{1}", 経路.from, 経路.to);
    }
  }
}
リスト1 コンパイルできないサンプル

……\Program.cs(20,52): エラー CS1525: 'from' は無効です。
……\Program.cs(20,57): エラー CS1031: 型が必要です。
……\Program.cs(20,60): エラー CS0742: クエリ本体の後には select 句または group 句が必要です。
……\Program.cs(20,65): エラー CS1002: ; が必要です。
……\Program.cs(20,73): エラー CS1002: ; が必要です。
……\Program.cs(20,73): エラー CS1525: ')' は無効です。
……\Program.cs(20,74): エラー CS1002: ; が必要です。
リスト1で発生するコンパイル・エラー

 多数のエラーが出ていて分かりにくいが、要するにwhere句の「n.from == "新宿"」という式で使われたfromは無効だということである。

 なぜ無効かといえば、fromはfrom句で使用されるキーワードだからである。しかし、ここが極めて悩ましいところなのだが、fromはC#の予約語だから使用できない、というわけではない。クエリ式の外部に記述されたfromは一切エラーの対象になっていないのである。

 これはC#の言語仕様拡張のポリシーによって発生した事態といえる。C#では、言語仕様が拡張され、新しいキーワードが導入されるとき、それを予約語扱いしないようになっている。そして、そのキーワードは新規に追加された構文内でのみ特別な意味を発揮するようになっているのである。クエリ式のキーワードも同じで、それに該当する以下のキーワードはクエリ式内でのみ特別な意味を持つようになっている。

from、join、on、equals、into、let、orderby、ascending、descending、select、group、by

 つまり、fromはクエリ式以外では普通に記述できて普通に機能する何の変哲もないキーワードであるにもかかわらず、クエリ式では使用することができない。

 このような問題を解決するために、C#はもともとプレフィックス「@」という機能を持っている。これは、意味のあるキーワードと重複するキーワードを記述する際、先頭に「@」記号を付けることで、特別な意味付けを発揮させないものである。

 上記のリスト1では、where句のfromの先頭に「@」を付ければよい。

where n.from == "新宿"
 
where n.@from == "新宿"

 これでコンパイルでき、実行できる。

新宿→京王八王子
新宿→橋本
リスト1の修正後の実行結果

 もっとも、このプレフィックス「@」はくせもので、悪用するとパッと見た目に反する動作をさせることもできてしまう。以下は、「@false」という名前の定数にtrueの値を割り当てることで、条件判断の見た目の印象を逆転させた例である。

using System;

class Program
{
  private const bool @false = true;

  private static void warn(bool ageOK)
  {
    // 年齢認証がfalseの場合は「ダメ!」
    Console.WriteLine(ageOK == @false ? "飲酒ダメ!" : "飲酒OK");
  }

  static void Main(string[] args)
  {
    int age = 17;     // 僕17歳
    warn(age >= 20);  // お酒は20歳になってから
    // 出力:飲酒OK
  }
}
リスト2 プレフィックス「@」の悪用例

 このように、プレフィックス「@」はクエリ式では必須の存在だが、どこで使ってもよいものではない。どうしても必要な場面だけで使うようにしよう。

メソッド形式のLINQ

 さて、ここからが本題である。

 実は、C#でLINQを使う方法はクエリ式だけではない。これまで長々とクエリ式の書き方を説明してきたが、それは唯一の方法ではない。それどころか、クエリ式とはLINQの全機能を使うことができない不完全なやり方でしかないのである。

 では、C#にはクエリ式とは別に、「完全版・真クエリ式」のような構文があるのだろうか? そうではない。そのような構文はC#には存在しない。

 実際に存在するのは、「メソッド形式」あるいは「メソッド構文」と呼ばれるLINQの記述方法である。これはC#の文法上は、何ら特別なものではなく、実体はただのメソッド呼び出しである。

 実際にその事例を見てみよう。まず、前々回に紹介した「最も基本的なLINQ」のサンプル・コードを再掲しよう。

using System;
using System.Linq;

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

    var query = from x in array select x;

    foreach (int n in query) Console.WriteLine(n);
    // 出力:
    // 1
    // 2
    // 3
  }
}
リスト3 最も基本的なLINQ

 このサンプル・コードのクエリ式の行は以下のようなメソッド形式に書き換えることができる。

var query = array.Select((x) => x);

 この書き換えは、機能的に完全に同等である。念のために強調するが、これは「同等の結果が得られる」という意味ではない。別の方法によって同じ結果を得るのでもないし、まして同じ結果になるような別のプログラムを組んだという意味でもない。

 上記の2つのコードは100%完全にあらゆる意味で同等なのである。それは、生成された実行ファイルの中身を調べれば明確に分かる。.NET Reflectorを使ってC#形式で逆コンパイルすると、まったく同じコードが生成されていることが分かる。

private static void Main(string[] args)
{
  IEnumerable<int> query
      = new int[] { 1, 2, 3 }
        .Select<int, int>(delegate (int x) { return x; });

  foreach (int n in query)
  {
    Console.WriteLine(n);
  }
}
リスト4 クエリ式を使用したケースの逆コンパイル結果

private static void Main(string[] args)
{
  IEnumerable<int> query
      = new int[] { 1, 2, 3 }
        .Select<int, int>(delegate (int x) { return x; });

  foreach (int n in query)
  {
    Console.WriteLine(n);
  }
}
リスト5 メソッド形式を使用したケースの逆コンパイル結果

 もちろん、C#ではなくMSILに逆アセンブルしても同じである。コンパイラが生成したIDの値が異なる以外、完全に同等のコードが生成されている。

 ではなぜこの2つはここまで完全に同等なのだろうか。その理由は実は簡単である。

 クエリ式とは実は単なる糖衣構文(シンタックス・シュガー)にすぎないからである。糖衣構文とは、書きやすくするために提供される方便としての構文でしかない。つまり、

from x in array select x;

というクエリ式は、機械的に以下のメソッド呼び出しに置換される。

array.Select((x) => x);

 これにより、この2つのコードは実行結果だけでなく、実行するコードそのものまで完全に同等という一致を見せてくれるのである。

■クエリ式とメソッド構文の違い

 あらためて以下の2つを見比べてみよう。

var query = from x in array select x;
クエリ式

var query = array.Select((x) => x);
メソッド形式

 この2つの間に存在する違いをまとめてみよう。

  • from句(from …… in ……)に相当する部分は、メソッド形式には存在しない。メソッド形式にfrom句に当たる特別な表記方法は存在しない。その部分は、メソッド形式の方が短くなる
  • メソッド形式は引数がデリゲートであるため、通常はラムダ式を書き込む必要がある(Select((x) => x))。ラムダ式なしで直接記述できるクエリ式(select x)の方が短く記述できる。また、ラムダ式を使いこなすスキルが要求される
  • 範囲変数xは消滅するが、ラムダ式の仮引数xが出現する

■絞り込みと結果の生成

 すでに説明したさまざまな句を含むクエリ式をメソッド形式に書き換えてみよう。

 まず、where句による結果の絞り込みとselect句による結果の生成を含むサンプル・コードを書き換えてみよう。

using System;
using System.Linq;

class 車両形式
{
  public bool 地下鉄乗り入れ可能 = false;
  public string 形式名;
  public int 導入年度;
}

class Program
{
  static void Main(string[] args)
  {
    車両形式[] 車両形式データ =
    {
      new 車両形式() { 形式名 = "6000系", 導入年度 = 1972,
                                        地下鉄乗り入れ可能 = true },
      new 車両形式() { 形式名 = "7000系", 導入年度 = 1984 },
      new 車両形式() { 形式名 = "8000系", 導入年度 = 1992 },
      new 車両形式() { 形式名 = "9000系", 導入年度 = 2001,
                                        地下鉄乗り入れ可能 = true },
    };

    var query = from x in 車両形式データ
                where x.地下鉄乗り入れ可能
                select new { x.形式名, x.導入年度 };

    foreach (var 形式 in query)
    {
      Console.WriteLine(
                "{0} {1}年導入", 形式.形式名, 形式.導入年度);
    }
    // 出力:
    // 6000系 1972年導入
    // 9000系 2001年導入
  }
}
リスト6 絞り込みと結果の生成

 上記プログラムのクエリ式は以下の行に書き換えることができる。

var query = 車両形式データ
            .Where((x) => x.地下鉄乗り入れ可能)
            .Select((x) => new { x.形式名, x.導入年度 });

 where句はWhereメソッドの呼び出しに置き換えられ、引数は条件式ではなくラムダ式に変化している。また、select句はSelectメソッドの呼び出しに置き換えられているが、もちろん型名を指定しないオブジェクト生成を引数に記述できる。

 この記述において、Whereメソッドの引数にあるラムダ式の仮引数xと、Selectメソッドの引数にあるラムダ式の仮引数xはまったく別個の存在となる。クエリ式では範囲変数xは1つだけ宣言されて使用されたが、ラムダ式の引数はそれぞれ別個に宣言し、別個に使用される必要がある。これも、クエリ式とメソッド形式の相違といえる。

■最初の事例の別解

 最初にリスト1で紹介した「コンパイルできないサンプル」の事例は、メソッド形式を使ってコンパイル可能にするという別解がある。

 具体的には、クエリ式の行を以下のようにメソッド形式に書き直すだけである。

foreach (var 経路 in
     移動経路データ.Where((n) => n.from == "新宿").Select((n) => n))

 これで、fromキーワードがおかしいとコンパイラがエラーを出すことはなくなる。

 コンパイルを通るのは、もちろん、fromキーワードが特別な意味を持つのはクエリ式の場合だけであり、これはクエリ式ではないからである。メソッド形式は言語仕様的には単なるメソッド呼び出しであり、クエリ式と同じ効能を発揮させているとしても、これはクエリ式ではない。

 

 INDEX
  C# 3.0入門
  第8回 LINQメソッド形式編
  1.予約語のエスケープ/メソッド形式のLINQ
    2.メソッド形式でのみ可能なクエリ/メソッド形式のソート/複数のソースのクエリ
    3.メソッド形式のクエリの接続/クエリ結果のグループ化
    4.メソッド形式のlet/句効率的に列挙可能にするという問題
 
インデックス・ページヘ  「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 記事ランキング

本日 月間