2014年、あけましておめでとうございます。
本年も、たーせるをよろしくお願いいたします。
昨年は某ITゼネコンの中枢にて炎上案件の消火活動に勤しんでおりました。
かろうじて鎮火に成功したものの満身創痍。もはやプログラミングをやっていた頃の脳細胞は死滅済みなので、リハビリも兼ねてちょっと C# に触ってみました。
使用した開発環境は、Visual Studio 2012 for Windows Desktop、プロジェクトはコンソールアプリケーションです。それではさっそくいってみましょう。
匿名型、からのLINQ to Objects
まずは以下の属性を持つテーブル(を表現する)データ構造を、匿名型で作ってみます。
- ID(識別番号)
- UserName(名前)
- Sex(性別)
Program.cs
using System; namespace CSharpConsoleApplicationSample { class Program { static void Main(string[] args) { // 名簿を作成 var list = new[] { new {ID = 0001, UserName = "@tercel_s", Sex = "Male"}, new {ID = 0002, UserName = "@linehat", Sex = "Male"}, new {ID = 0003, UserName = "@pi_cro_s" , Sex = "Female"} }; // 内容を表示 foreach (var record in list) Console.WriteLine("{0} \t{1} \t{2}", record.ID, record.UserName, record.Sex); } } }
ここまでは、かんたんかんたん。
実行結果
次に、この名簿から、名前(UserName)の末尾が s の要素(レコード)だけを抽出したいと思います。
一昔ほど前までは、以下のように if 文と条件式でこんなふうに書くのが当たり前でした。
// 内容を表示 foreach (var record in list) if (record.UserName.EndsWith("s")) // もし、名前の末尾が s ならば Console.WriteLine("{0} \t{1} \t{2}", // 当該レコードの内容を表示 record.ID, record.UserName, record.Sex);
しかしC#には、所望の要素(レコード)のみを抽出するための特殊な構文(クエリ式)が用意されています。
using System; using System.Linq; // 追加 namespace CSharpConsoleApplicationSample { class Program { static void Main(string[] args) { // 名簿を作成 var list = new[] { new {ID = 0001, UserName = "@tercel_s", Sex = "Male"}, new {ID = 0002, UserName = "@linehat", Sex = "Male"}, new {ID = 0003, UserName = "@pi_cro_s" , Sex = "Female"} }; // 名前の末尾が s のレコードを抽出 const string END_STRING = "s"; var query = from x in list where x.UserName.EndsWith(END_STRING) select new {x.ID, x.UserName, x.Sex}; // 内容を表示 foreach (var record in query) Console.WriteLine("{0} \t{1} \t{2}", record.ID, record.UserName, record.Sex); } } }
実行すると、うまく名前の末尾が s の要素のみを抽出することができます。
ID | UserName | Sex |
1 | @tercel_s | Male |
3 | @pi_cro_s | Female |
わーい。
ちなみに、ORDER BYやGROUP BY相当の処理もお手の物。たとえば、下記のコードでデータの並び順を簡単にソートできてしまうのです。
// IDで降順 → 名前で昇順にソートして表示 var query = from x in list orderby x.ID descending, x.UserName ascending select new { x.ID, x.UserName, x.Sex }; foreach (var record in query) Console.WriteLine("{0} \t{1} \t{2}", record.ID, record.UserName, record.Sex);
実行するとこんな感じになります。
ID | UserName | Sex |
3 | @pi_cro_s | Female |
2 | @linehat | Male |
1 | @tercel_s | Male |
これのどこがすごいかというと、「誰が書いてもだいたい同じコードになる処理」を簡潔に書ける点だと思います。
たとえば、ビジネスアプリとかで明細画面を作るときには、表示項目(アトリビュート)の可視状態を切り替えたり、月次の売上の合計額を画面に出したりするコードを書くことがあります。そんなとき、for文でくるくる回すのは少々ナンセンス。いや、べつにfor文が悪いとは言わないけれど、「それが何のためのfor文なのか」が明確じゃないコードで画面が埋め尽くされるのは、プログラムをメンテナンスする側にとってもストレスなわけです。
閑話休題。
次節はもう少し掘り下げて、2つのテーブルの結合を見てみたいと思います。
結合とか
ここからは、以下の2つのテーブルを使用するものとします。売上テーブルの設計はもうちょっとなんとかなっただろうとは思いますが、練習用なので大目に見てください。
なお、顧客テーブルの顧客IDは売上テーブルの顧客IDに1対多で紐づいていると仮定します。
- 顧客テーブル
顧客ID | 顧客名 |
0001 | たーせる |
0002 | ボブ |
0003 | クリスティ |
- 売上テーブル
売上No | 顧客ID | 商品名 |
0001 | 0001 | フレンチフライ |
0002 | 0002 | フレンチフライ |
0003 | 0001 | スカイフィッシュバーガー |
0004 | 0004 | コーラ |
上記のテーブルをC#で書くと以下のような感じでしょうか。
本来はプログラムの外にデータソースがあって、そこからプログラムにデータをインポートした直後を再現しています。
using System; namespace CSharpConsoleApplicationSample { class Program { static void Main(string[] args) { // テーブルをつくる var 顧客テーブル = new[] { new {顧客ID = 0001, 顧客名 = "たーせる"}, new {顧客ID = 0002, 顧客名 = "ボブ"}, new {顧客ID = 0003, 顧客名 = "クリスティ"} }; var 売上テーブル = new[] { new {売上No = 0001, 顧客ID = 0001, 商品名 = "フレンチフライ"}, new {売上No = 0002, 顧客ID = 0002, 商品名 = "フレンチフライ"}, new {売上No = 0003, 顧客ID = 0001, 商品名 = "スカイフィッシュバーガー"}, new {売上No = 0004, 顧客ID = 0004, 商品名 = "コーラ"} }; } } }
内部結合
内部結合ってなんだったっけ? SQL ではこんな感じで書きます。
SELECT * FROM 顧客テーブル, 売上テーブル WHERE 顧客テーブル.顧客ID = 売上テーブル.顧客ID
ちなみに以下のようにも書ける。
SELECT * FROM 顧客テーブル INNER JOIN 売上テーブル ON 顧客テーブル.顧客ID = 売上テーブル.顧客ID
このSQLの処理結果のイメージはこんなふうになるはずです。
顧客ID | 顧客名 | 売上No | 商品名 |
0001 | たーせる | 0001 | フレンチフライ |
0001 | たーせる | 0003 | スカイフィッシュバーガー |
0002 | ボブ | 0002 | フレンチフライ |
C#だと、こんな感じですにゃ。SQLの面影がやや残っています。
// 内部結合 var query1 = from k in 顧客テーブル join u in 売上テーブル on new{k.顧客ID} equals new {u.顧客ID} select new{k.顧客ID, k.顧客名, u.売上No, u.商品名}; // 別解 var query2 = 顧客テーブル.Join(売上テーブル, k => new {k.顧客ID}, u => new {u.顧客ID}, (k, u) => new {k.顧客ID, k.顧客名, u.売上No, u.商品名}); // 結果表示 foreach (var record in query1) { Console.WriteLine("{0} \t{1} \t{2} \t{3}", record.顧客ID, record.顧客名, record.売上No, record.商品名); }
二つ例を示しましたが、前者が糖衣構文(クエリ式)を使った記法で、後者は純粋にC#の通常の構文にラムダ式をあしらった形になります。
外部結合
続いて、(左)外部結合です。
SELECT * FROM 顧客テーブル LEFT OUTER JOIN 売上テーブル ON 顧客テーブル.顧客ID = 売上テーブル.顧客ID
このSQLの処理結果のイメージはこんなふうになるはずです。
顧客ID | 顧客名 | 売上No | 商品名 |
0001 | たーせる | 0001 | フレンチフライ |
0001 | たーせる | 0003 | スカイフィッシュバーガー |
0002 | ボブ | 0002 | フレンチフライ |
0003 | クリスティ | - | - |
ちなみに、上の表の - は NULL 値を表しています。
こいつを C# で書くと、こんな感じ。
// 外部結合 const string NULL_VALUE = "-"; var query = from k in 顧客テーブル join u in 売上テーブル on k.顧客ID equals u.顧客ID into tmp from x in tmp.DefaultIfEmpty() select new { k.顧客ID, k.顧客名, 売上No = x == null ? NULL_VALUE : x.売上No.ToString(), 商品名 = x == null ? NULL_VALUE : x.商品名.ToString() }; // 結果表示 foreach (var record in query) { Console.WriteLine("{0} \t{1} \t{2} \t{3}", record.顧客ID, record.顧客名, record.売上No, record.商品名); }
実は上記のコード、ちょっと気に食わない点があります。
(左)外部結合を行うとクリスティさんに紐づく売上データが無いため、彼女の売上Noや商品名は必然的に NULL 値となります。このNULLの扱いが非常にいやらしい。
匿名型の要素には NULL を代入できないので、上記コードでは苦肉の策として NULL を表す文字列型の定数を作りました……が、これ本当はあまりよくないやり方です。
NULLのせいで 売上No の型情報が壊されてしまっているので、後々やっかいなバグを生む可能性が大きいからです。
いくらか制約条件を設けて(たとえば「売上Noに負数は存在しない・負数の場合は NULL 値とみなす(またはDouble.NaNを使う)」等)、結果表示側に一手間加えれば型情報を壊さずに済みますが、それだとソースの可読性が下がって意図を理解してもらいにくくなると考えます。
うーん、、、
こういうとき、えらい人はどう書くんだろう……。