var query = from person in people join pet in pets on person equals pet.Owner into gj from subpet in gj.DefaultIfEmpty() select new { person.FirstName, PetName = (subpet == null ? String.Empty : subpet.Name) };
ちなみにこれは、MSDNの「方法 : 左外部結合を実行する (C# プログラミング ガイド)」から。
大体どこを見てもこんな感じで、ほとんど左外部結合をしたいときの慣用句(イディオム)みたいなもののよう。とはいえ、このコードを見て「あ、左外部結合させたいんだな」と思える人がどれくらいいるか。要は、コードから意図が解りづらいので、あまり好みじゃないなぁ。と。
もう少し、コードを見て左外部結合であることが解るように、拡張メソッドを作ってみた。こんなの。
public static class EnumerableEx { public static IEnumerable<TResult> OuterJoin<TOuter, TInner, TKey, TResult> (this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector, TInner innerDefaultValue) { return outer .GroupJoin( inner, outerKeySelector, innerKeySelector, (o, i) => new { Out = o, Ins = i.DefaultIfEmpty(innerDefaultValue) }) .SelectMany(g => g.Ins, (g, i) => resultSelector(g.Out, i)); } }
Enumerable.Joinメソッドと比べると、最後のパラメータが余計についてます。これはOuterの要素に対して合致するInnerの要素がなかった時の代替要素を指定するもの。Null Objectだと思っておけばよいと思います。
あと、名前は長いのを嫌い、「OuterJoin」としています。タイプパラメータからも、左が外なのは明らかなので、特に問題はないと思っています。
ちなみに、これをライブラリ的に用意するなら、GroupJoinするときにEqautityComparerを指定するパターン、それと、DefaultIfEmptyでTInnerのデフォルト値を自動的に使うパターン、その組み合わせで4つのオーバーロードを、用意しておくのがよいと思います。
さて、テストします。
static void Main(string[] args) { var products = new[] { new { ProductId = 1, Name = "えんぴつ" }, new { ProductId = 2, Name = "けしごむ" }, new { ProductId = 3, Name = "コンパス" }, new { ProductId = 4, Name = "クレヨン" }, }; var sales = new[] { new { SaleId = 1, ProductId = 1, Buyer = "○○商会", Quantity = 2 }, new { SaleId = 2, ProductId = 1, Buyer = "××文具店", Quantity = 4 }, new { SaleId = 3, ProductId = 2, Buyer = "△屋", Quantity = 3 }, new { SaleId = 4, ProductId = 4, Buyer = "○○商会", Quantity = 1 }, new { SaleId = 5, ProductId = 5, Buyer = "○○商会", Quantity = 1 }, }; var nosaled = new { SaleId = 0, ProductId = 0, Buyer = "(no sales)", Quantity = 0 }; var salesInfo = products.OuterJoin( sales, p => p.ProductId, s => s.ProductId, (p, s) => new { p.Name, s.Buyer, s.Quantity }, nosaled); foreach (var i in salesInfo) { Console.WriteLine("{0}, {1}, {2}", i.Name, i.Buyer, i.Quantity); } }
結果はこう。
えんぴつ, ○○商会, 2 えんぴつ, ××文具店, 4 けしごむ, △屋, 3 コンパス, (no sales), 0 クレヨン, ○○商会, 1
…右外部結合?RIGHT OUTER JOINか。僕自身使ったことないですが、必要なら右左を入れ替えてあげればいいはず。上のコードの20行目から28行目を、こんな感じに置き換えてみる。
var noproduct = new { ProductId = 0, Name = "(no item)" }; var salesInfo = sales.OuterJoin( products, s => s.ProductId, p => p.ProductId, (s, p) => new { p.Name, s.Buyer, s.Quantity }, noproduct);
結果はこう。
えんぴつ, ○○商会, 2 えんぴつ, ××文具店, 4 けしごむ, △屋, 3 クレヨン, ○○商会, 1 (no item), ○○商会, 1
うん。いんじゃないかな?
…完全外部結合?…FULL OUTER JOIN…。必要?それ。
使い道が解らなくてモチベーションゼロだけど、要するに左外部結合+右のみに存在するレコードを、編集してUnionすればいいんじゃないかな?多分。