Ver. 3.0
2005年9月、C# 2.0 の正式出荷を目前にして、 C# の次世代拡張 C# 3.0 の言語仕様が公開されました。
C# 3.0(そして、同時に発表された VB 9.0)の目玉となる機能は、 Language Integrated Query、略して LINQ と呼ばれるもので、 リレーショナルデータベースや XML に対する操作をプログラミング言語に統合するものです。 (データベースや XML 操作用のライブラリと、 プログラミング言語中にSQL 風の問い合わせ構文を埋め込めるようにする言語拡張から成ります。)
C# 3.0 に追加された機能は、基本的にこの LINQ を使うために必要な機能、 あるいは、より便利に LINQ を使うための機能になります。
LINQ とは、 Language Integrated Query の略称で、 C# や VB などの .NET Framework 対応言語に、 リレーショナルデータや XML に対するデータ操作構文を組み込む (+ データベースや XML 操作用のライブラリ) というものです。
SQL などのデータベース操作言語をご存知の方ならば、 すぐに LINQ になじむこと出来るでしょう。 この手の言語のことをご存じない方のために、 SQL を例に、簡単な説明をしたいと思います。
まず、操作対象の例として、 表1に示すような、「学生名簿」と言う名前のデータテーブルを考えます。
表1: データテーブル: 学生名簿
| 出席番号 | 姓 | 名 |
|---|---|---|
| 14 | 風浦 | 可符香 |
| 20 | 小森 | 霧 |
| 22 | 常月 | まとい |
| 19 | 小節 | あびる |
| 18 | 木村 | カエレ |
| 16 | 音無 | 芽留 |
| 17 | 木津 | 千里 |
| 8 | 関内 | マリア |
| 28 | 日塔 | 奈美 |
こういったデータテーブルの中から、 特定の条件を満たすものだけを取り出したり、 順番を並べ替えたりという操作を行うのがデータベース操作言語です。 例えば、SQL を使って、 このテーブル中から、 出席番号前半(15以下)の学生だけ、 出席番号の小さい順に並べ、 その「名」を取り出したい場合、 以下のような問い合わせを書きます。
SELECT 名 FROM 学生名簿 WHERE 出席番号 <= 15 ORDER BY 出席番号;
この問い合わせの結果は、 表2のようになるでしょう。
表2: 問い合わせ結果: 出席番号前半の「名」
| 名 |
|---|
| マリア |
| 可符香 |
SELECT などのキーワードに関して、 簡単に述べると以下のようになります。
表3: SQL のキーワード
| キーワード | 説明 |
|---|---|
| SELECT | 姓、名、学生番号などのうち、どれを表示するか |
| FROM | どのデータテーブルからデータを読むか |
| WHERE | 取り出したいデータに対する条件 |
| ORDER BY | 取り出す順番 |
さらに、複数のテーブルに渡る問い合わせも可能です。 先ほど表1で示したデータテーブルに加え、 表3のようなデータテーブルもあったとしましょう。
表4: データテーブル: 備考欄
| 出席番号 | 備考 |
|---|---|
| 19 | しっぽ好き |
| 19 | 被 DV 疑惑 |
| 16 | 毒舌メール |
| 17 | 几帳面 |
で、この2つのテーブル「学生名簿」と「備考欄」に対して、 以下のような問い合わせ操作をしてみます。
SELECT 姓, 名, 備考 FROM 学生名簿, 備考欄 WHERE 学生名簿.学生番号 == 備考欄.学生番号
その結果得られるデータは以下のようになります。
表5: 問い合わせ結果: 姓名と備考
| 姓 | 名 | 備考 |
|---|---|---|
| 小節 | あびる | しっぽ好き |
| 小節 | あびる | 被 DV 疑惑 |
| 音無 | 芽留 | 毒舌メール |
| 木津 | 千里 | 几帳面 |
本当に簡単にですが、データベース操作言語の概要を述べた所で、 LINQ の話に戻りましょう。 改めて書きますが、LINQ とは、 C# 等の言語に SQL ライクなデータベース操作構文を組み込む (+ データベースや XML 操作用のライブラリ) というものです。
百聞は一見にしかずということで、 とりあえず、先ほどの SQL での例を C# 3.0 の構文を使って書いてみましょう。
var 学生名簿 = new[] { new {学生番号 = 14, 姓 = "風浦", 名 = "可符香"}, new {学生番号 = 20, 姓 = "小森", 名 = "霧" }, new {学生番号 = 22, 姓 = "常月", 名 = "まとい"}, new {学生番号 = 19, 姓 = "小節", 名 = "あびる"}, new {学生番号 = 18, 姓 = "木村", 名 = "カエレ"}, new {学生番号 = 16, 姓 = "音無", 名 = "芽留" }, new {学生番号 = 17, 姓 = "木津", 名 = "千里" }, new {学生番号 = 8, 姓 = "関内", 名 = "マリア"}, new {学生番号 = 28, 姓 = "日塔", 名 = "奈美" }, }; var 学籍番号前半名 = from p in 学生名簿 where p.学生番号 <= 15 orderby p.学生番号 select p.名; foreach(var 名 in 学籍番号前半名) { Console.Write("{0}\n", 名); }
マリア 可符香
非常に見慣れない構文だらけで困惑するかもしれませんが、 C# 3.0 ではこのコードがコンパイル可能です。 というより、これがコンパイル可能となるような、新しい構文が追加されました。 詳細は次節以降で述べていくことになります。
ちなみに、簡単に解説だけしておくと、
第1文、var 学生名簿 の部分では、
SQL の説明で述べた所のデータテーブルを作っています。
C# 3.0 の新機能としては、「型推論」や「匿名型」などの機能を使っています。
第2文、var 学籍番号前半姓 の部分では、
C# 3.0 の目玉となる機能、LINQ を使っています。
where や select などの「問い合わせ式」を用いて、
データ操作を行っています。
最後の部分、foreach 以下は、
C# 2.0 までの感覚でも割となじみやすいのではないかと思います。
問い合わせ結果の一覧を画面に表示しています。
var キーワードを用いて、 暗黙的に型付けされたローカル変数変数(Implicitly typed local variables)を定義できるようになりました。
var n = 1; var x = 1.0; var s = "test";
var を用いる際には、必ず初期値を伴う必要があります。
そして、初期値から、変数の型を自動判別してくれます。
上記の例では、
n は int、
x は double、
s は string 型の変数になります。
注意すべき点は、 あくまで型の自動判別・推定であって、 任意の型の値を代入できる万能な変数を作れるわけではないということです。 したがって、以下のように、初期値を伴わない宣言は(型の推定ができないので)エラーになります。
var n; // エラー。初期値が必要。
ちなみに、この機能、後述する匿名型と併せて、 LINQ をより便利に使うためのものであって、 それ以外の場面ではあまり乱用すべきではないと思います。 型の自動判別・推定機構に頼らず、できる限りちゃんと型を明示すべきです。 むやみに型推定機能に頼ると、知らず知らずの間に int のつもりで使っていた変数が double になっていたということも起きかねません。
C# 2.0 までの常識で言うと、 既存のクラスの機能拡張(=メソッドの追加)をしたければ、 そのクラスを継承したりなどして、新しいクラスを作るしかありませんでした。
これに対して、C# 3.0 では、後述する方法で、 既存のクラスにメソッドを追加できます。 (正確には、インスタンスメソッドの“ようなもの”。インスタンスメソッドと同じ構文で呼べるだけ。) このような、後から追加するメソッドのことを拡張メソッド(extension method)と呼びます。
まず、拡張メソッドの定義の仕方ですが、 以下のように、 静的クラス中に、 第一引数に this キーワードを修飾子として付けた static メソッドを書きます。
static class StringExtensions { public static string ToggleCase(this string s) 中身省略 }
このようにして定義したメソッドは、 通常通り、静的メソッドとして呼び出すこともできますが、 あたかも string 型のインスタンスメソッドであるかのように呼び出せるようになります。
string s = "This Is a Test String."; string s1 = StringExtensions.ToggleCase(s); // 通常の呼び出し方。 string s1 = s.ToggleCase(); // 拡張メソッド呼び出し。
上述のような拡張メソッドの利用例のソース全てを以下に示します。
using System; namespace ConsoleApplication1 { static class StringExtensions { /// <summary> /// 文字列の大文字と小文字を入れ替える。 /// </summary> /// <param name="s">変換元</param> /// <returns>変換結果</returns> public static string ToggleCase(this string s) { System.Text.StringBuilder sb = new System.Text.StringBuilder(); foreach(char c in s) { if(char.IsUpper(c)) sb.Append(char.ToLower(c)); else if(char.IsLower(c)) sb.Append(char.ToUpper(c)); else sb.Append(c); } return sb.ToString(); } } class ExtensionMethodTest { static void Main(string[] args) { string s = "This Is a Test String."; Console.Write(s.ToggleCase()); } } }
tHIS iS A tEST sTRING.
ちなみに、この機能も、 どうしても必要になった場合を除いて、あまり使うべきではないでしょう。 private, protected なメンバにアクセスできないなど、 通常のインスタンスメソッドと比べて機能的な制約がありますし、 クラス本体と別の場所にメソッド定義があるため、 定義された場所を探すのに苦労する可能性があります。
ただ、1つだけ、拡張メソッドにも、通常のインスタンスメソッドよりも優位な点があります。 それは、インターフェースに対して、 インスタンスメソッド風のメソッドを定義できると言うことです。 通常、インターフェースは、メソッドの外部仕様のみを定義でき、 実装は定義できません。 しかしながら、拡張メソッドを利用することで、 インスタンスメソッド定義っぽいことが実現できます。
ラムダ式(lambda expression)と言うのは、 関数型言語と呼ばれるような種類のプログラミング言語における用語なのですが、 関数(メソッド)を整数などの変数と全く同列に扱う手法のことです。
ただ、C# 3.0 で導入されたラムダ式は、 C# 2.0 の匿名メソッドをさらに簡便な記法で書けるようにしたもので、 関数型言語のラムダ式の機能性と比べると見劣りするものです。 見劣りするというか、 汎用プログラム言語に関数型言語ほどのラムダ式の機能性を求めるのは間違いというか、 むしろ、パフォーマンスの面から考えると不要というか。 とりあえず、匿名メソッドの発展形くらいに思っておいた方がいいかと思います。
匿名メソッドは、例えば、以下のような書き方をしていました。
delegate(int n) { return n > 0; }
この例の匿名メソッドをラムダ式を使って書き直すと、以下のようになります。
(int n) => n > 0;
要するに、記法としては、以下のようになります。
引数リスト => 式
ちなみに、文脈的に引数の型が明らかな場合、 型は省略できます。 (型推定機能がある。) 例えば、以下のようなデリゲートがあるとき、
delegate bool Pred(int n);
このデリゲートに対する代入式中にラムダ式を書く場合、 引数の型は int であることが明らかなので、 int を省略して以下のように書くことができます。
Pred p = s => s > 0;
あと、匿名メソッドと違って、 ラムダ式は本当にデータとして扱うこともできます。
上述の例の
Pred p = s => s > 0; のように、
デリゲートに代入する場合には、
ラムダ式は匿名メソッドと同じ扱い、
すなわち、コンパイル後には実行コードの状態になっています。
これに対して、ラムダ式を Expression 型の変数に代入すると、データとして扱うことができ、 以下のように式中の項を取り出したりといった操作が可能です。
Expression<Func<int, bool>> e = n => n > 0; BinaryExpression lt = (BinaryExpression) e.Body; ParameterExpression en = (ParameterExpression) lt.Left; ConstantExpression zero = (ConstantExpression) lt.Right;
オブジェクトの初期化を以下のような記法でできるようになりました。
Point p = new Point{ X = 0, Y = 1 };
ちなみに、このコードは以下のようなコードと等価です。
Point p = new Point();
p.X = 0;
p.Y = 1;
また、コレクションの初期化を以下のような記法でできるようになりました。
List<int> list = new List<int> {1, 2, 3};
要するに、配列と同じような初期化記法を、任意のコレクションクラスに対して行うことができます。 ちなみに、このコードは以下のようなコードと等価です。
List<int> list = new List<int>(); list.Add(1); list.Add(2); list.Add(3);
C# 3.0 では匿名型(anonymous type)を作成できるようになりました。 匿名型の作り方は以下の通りです。
var x = new { FamilyName = "糸色", FirstName="望"};
このようなコードから、自動的に、以下のような型が生成されます。
class __Anonymous1 // この __Anonymous という名前はプログラマが参照できるわけではない。 { private string f1; private string f2; public string FamilyName { get { return this.f1} set { this.f1 = value} }; public string FirstName { get { return this.f2} set { this.f2 = value} }; }
そして、変数 x に対して、 2つのプロパティ FamilyName と FirstName が使えます。
var x = new { FamilyName = "糸色", FirstName="望"}; Console.Write("{0}\n", x.FamilyName, x.FirstName);
まあ、匿名クラスは、その場限りの使い捨てなクラスになるわけで、 普通はあまり使うような機能ではありません。 基本的には、LINQ のための機能だと思っていいでしょう。 例えば、後述する問い合わせ式中で、以下のように利用します。
var list1 = from p in list where p.id <= 15 orderby p.id select new { p.FamilyName, p.FirstName };
new で配列を作成する際、 型を省略できるようになりました。
int[] array = new[] {1, 2, 3, 4};
見ての通り、 new の後ろの型を省略しています。 配列の型は、{} の中身の型から推定されます。 この例の場合、中身が 1, 2, 3, 4 といずれも int 型なので、 配列は int[] 型になります。
まあ、これだけだと、 ちょっとタイピングをサボれる程度ですが、 var および匿名型と組み合わせることによって、 配列の暗黙的の真価が発揮されます。
var array = new[] { new {X = 0, Y = 1}, new {X = 3, Y = -1}, new {X = 7, Y = 3}, new {X = 13, Y = -5}, }; foreach(var p in array) Console.Write("{0}\n", p);
C# 3.0 の目玉となる機能は、 データベース操作言語 SQL 風の問い合わせ式(query expression)です。 すでに何度か例を示していますが、 以下のように、SQL 風の式を C# ソースファイル中に直接書ける機能です。
var list1 = from p in list where p.id <= 15 orderby p.id select new { p.FamilyName, p.FirstName };
この SQL 風の問い合わせ式は、 実は、C# のコンパイラ自体にデータ問い合わせの機構が埋め込まれているわけではありません。 C# 3.0 のコンパイラは、問い合わせ式をメソッド(あるいは拡張メソッド)呼び出しに変換します。 例えば、以下のような問い合わせ式を考えます。
var list1 = from p in list where p.id <= 15 select p.Name;
これは、C# 3.0 コンパイラによって、以下のように解釈されます。
var list1 = list.Where(p => p <= 15).Select(p => p.Name);
そして、実際のデータ問い合わせはこの Where や Select などのメソッド(あるいは拡張メソッド)内で行われます。 問い合わせ構文は、Where や Select などのメソッドを持つか、 あるいは、拡張メソッドによってこれらのメソッドを追加した、 任意のクラスに対して利用できます。
ちなみに、現状、この機能は、 System.Array や、 System.Collections.Generic.List<T> などのクラスには これらのメソッド定義があるわけではなく、 IEnumerable インタフェースの拡張メソッドとして定義されています。 拡張メソッドの定義場所は、System.Query.Sequence クラスです。
問い合わせ式中は、 where, select, group, orderby という4つの節から成ります。
これらの説明のために、 以下のようなデータを考えます。
var a = new[] { new {X = 0, Y = 1}, new {X = 0, Y = 2}, new {X = 0, Y = 3}, new {X = 3, Y = -1}, new {X = 3, Y = -2}, new {X = 3, Y = -3}, new {X = 7, Y = 3}, new {X = 13, Y = -5}, };
where 節で、取り出したいデータの条件を指定します。 以下に、where の例、その出力結果、および、コンパイラによる問い合わせ構文の解釈結果を示します。
var b = from p in a where p.Y > 0 // この条件を満たすものだけ取り出す select p; foreach(var p in b) Console.Write("{0}\n", p);
{X=0, Y=1}
{X=0, Y=2}
{X=0, Y=3}
{X=7, Y=3}
var b = a.Where(p => p.Y > 0);
select 節で、どういう形式でデータを出力するかを選択します。 以下に、select の例、その出力結果、および、コンパイラによる問い合わせ構文の解釈結果を示します。
var b = from p in a select p.Y; // Y だけ取り出す。 foreach(var p in b) Console.Write("{0} ", p);
1 2 3 -1 -2 -3 3 -5 0
var b = a.Select(p => p.Y);
group 節(group, by)で、値をグループ化することができます。 以下に、group, by の例、その出力結果、および、コンパイラによる問い合わせ構文の解釈結果を示します。
var b = from p in a group p.Y by p.X; // X の値が同じ Y をグループ化 foreach(System.Query.Grouping<int, int> p in b) { Console.Write("{0} -> (", p.Key); foreach(int i in p.Group) Console.Write(" {0}", i); Console.Write(" )\n"); }
0 -> ( 1 2 3 ) 3 -> ( -1 -2 -3 ) 7 -> ( 3 ) 13 -> ( -5 )
var b = a.GroupBy(p => p.X, p => p.Y);
orderby 節で、値のソートができます。 以下に、orderby の例、その出力結果、および、コンパイラによる問い合わせ構文の解釈結果を示します。
var b = from p in a orderby p.Y select p.Y; foreach(var p in b) Console.Write("{0} ", p);
-5 -3 -2 -1 1 2 3 3
var b = a.OrderBy(p => p.Y).Select(p => p.Y);
ちなみに、
orderby p.Y descending とすると、逆順にソートできます。
(ascending というキーワードもありますが、省略しても ascending と同じ動作になります。)
select 節もしくは group 節の後ろの into 節をつけることで、問い合わせの中間結果を一時変数に格納できます。
var b = from p in a group p.Y by p.X into g // gourp by の結果が g に格納されます。 select new { X = g.Key, YList = g.Group}; // その g を使って select 節を書く。 foreach(var p in b) { Console.Write("{0} -> (", p.X); foreach(int i in p.YList) Console.Write(" {0}", i); Console.Write(" )\n"); }
0 -> ( 1 2 3 ) 3 -> ( -1 -2 -3 ) 7 -> ( 3 ) 13 -> ( -5 )
from 節には複数のデータを , で区切って指定できます。
var a = new[] { new {X = 0, Y = 1}, new {X = 3, Y = -2}, new {X = 0, Y = 3}, new {X = 3, Y = -3}, new {X = 13, Y = -5}, }; var b = new[] { new {Y = 2, Z = 1}, new {Y = 3, Z = 2}, new {Y = -3, Z = 3}, new {Y = 6, Z = 4}, new {Y = -5, Z = 5}, }; var c = from p in a, q in b where p.Y == q.Y select new { p.X, q.Z}; foreach(var p in c) Console.Write("{0}\n", p);
{X=0, Z=2}
{X=3, Z=3}
{X=13, Z=5}
using System; using System.Query; class ExtensionMethodTest { static void Main(string[] args) { // データテーブルを2つほど定義。 var studentList = new[] { new {id = 0, 姓 = "糸色", 名 = "望" }, new {id = 14, 姓 = "風浦", 名 = "可符香"}, new {id = 20, 姓 = "小森", 名 = "霧" }, new {id = 22, 姓 = "常月", 名 = "まとい"}, new {id = 19, 姓 = "小節", 名 = "あびる"}, new {id = 18, 姓 = "木村", 名 = "カエレ"}, new {id = 16, 姓 = "音無", 名 = "芽留" }, new {id = 17, 姓 = "木津", 名 = "千里" }, new {id = 8, 姓 = "関内", 名 = "マリア"}, new {id = 28, 姓 = "日塔", 名 = "奈美" }, }; var remarks = new[] { new {id = 0, 備考="超ネガティブ"}, new {id = 14, 備考="超ポジティブ"}, new {id = 20, 備考="ひきこもり"}, new {id = 22, 備考="超恋愛体質"}, new {id = 22, 備考="ストーカー"}, new {id = 19, 備考="しっぽ好き"}, new {id = 19, 備考="被DV疑惑"}, new {id = 18, 備考="人格バイリンガル"}, new {id = 16, 備考="毒舌メール"}, new {id = 17, 備考="几帳面"}, new {id = 17, 備考="粘着質"}, new {id = 8, 備考="不法入国"}, new {id = 8, 備考="難民"}, new {id = 28, 備考="普通"}, }; // 2つのテーブルをくっつけてみる。 var remarksWithName = from s in studentList, r in remarks where s.id == r.id orderby s.id select new { 姓名 = s.姓 + s.名, r.備考 } into t1 group t1.備考 by t1.姓名 into t2 select new { 姓名 = t2.Key, 備考 = t2.Group }; // 結果の表示。 foreach(var s in remarksWithName) { Console.Write("{0} : ", s.姓名); foreach(var r in s.備考) Console.Write("{0} ", r); Console.Write("\n"); } } }
糸色望 : 超ネガティブ 関内マリア : 不法入国 難民 風浦可符香 : 超ポジティブ 音無芽留 : 毒舌メール 木津千里 : 几帳面 粘着質 木村カエレ : 人格バイリンガル 小節あびる : しっぽ好き 被DV疑惑 小森霧 : ひきこもり 常月まとい : 超恋愛体質 ストーカー 日塔奈美 : 普通
問い合わせ式の中に、入れ子で問い合わせ式を書くことができます。 と仕様書には書かれてるんだけど、コンパイル通らない・・・。 仕様書に書かれてるコードをそのまま書くと、以下のような感じです。
from c in customers where c.City == "London" from o in c.Orders where o.OrderDate.Year == 2005 select new { c.Name, o.OrderID, o.Total }
customers. Where(c => c.City == "London"). SelectMany(c => c.Orders. Where(o => o.OrderDate.Year == 2005). Select(o => new { c.Name, o.OrderID, o.Total }) );