LINQで遊ぶ

2014年、あけましておめでとうございます。

本年も、たーせるをよろしくお願いいたします。


昨年は某ITゼネコンの中枢にて炎上案件の消火活動に勤しんでおりました。

かろうじて鎮火に成功したものの満身創痍。もはやプログラミングをやっていた頃の脳細胞は死滅済みなので、リハビリも兼ねてちょっと C# に触ってみました。

使用した開発環境は、Visual Studio 2012 for Windows Desktop、プロジェクトはコンソールアプリケーションです。それではさっそくいってみましょう。

匿名型、からのLINQ to Objects

まずは以下の属性を持つテーブル(を表現する)データ構造を、匿名型で作ってみます。

  1. ID(識別番号)
  2. UserName(名前)
  3. 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);
        }
    }
}

ここまでは、かんたんかんたん。

実行結果

f:id:tercel_s:20140106191624p:plain


次に、この名簿から、名前(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を使う)」等)、結果表示側に一手間加えれば型情報を壊さずに済みますが、それだとソースの可読性が下がって意図を理解してもらいにくくなると考えます。

うーん、、、

こういうとき、えらい人はどう書くんだろう……。

Copyright (c) 2012 @tercel_s, @iTercel, @pi_cro_s.