LINQでの外部結合

LINQ-言語統合クエリの楽しさを楽しむために、C#でどれだけ発明されたか。つまり:

  • ジェネリック
  • 拡張メソッド
  • ラムダ式
  • 式の木
  • Anonumusタイプ
  • オブジェクト初期化子
  • 型推論

これで、次のようなものを書くことができます。
	var query = 
		from itemA in listA
		join itemB in listB
			on itemA.Key equals itemB.Key
		select new {itemA, itemB};
	

人は反対することはできません-印象的。

そして、このすべての構文糖の真ん中に、私が十分な睡眠をとることを妨げる露のスプーンがありました:)

これはOUTER JOINのサポートの完全な欠如です。しかし、それが判明したので、タールは簡単に変わります...ターン...ターン...

...別の構文糖に。

インターネットでLEFT OUTER JOINの解決策を見つけようとした人は、おそらく同じような解決策を知っています。

	var query = 
		from itemA in listA
		join itemB in listB
			on itemA.Key equals itemB.Key into outer
		from itemO in outer.DefaultIfEmpty()
		select new {itemA, itemO};
	

そのような設計は、桁違いに理解を明らかに混乱させ、すでに単純な構造を複雑にします。これは、INNER JOINをLEFT OUTER JOINに置き換えるだけです。ショックを与えないために、FULL OUTER JOINの例は示しません。

次のように書くことができれば、それは単純であるかのように見えます。

	var query = 
		from itemA in listA
		left join itemB in listB
			on itemA.Key equals itemB.Key
		select new {itemA, itemB};
	

とか、ぐらい

	var query = 
		from itemA in listA
		full join itemB in listB
			on itemA.Key equals itemB.Key
		select new {itemA, itemB};
	

しかし、違います。C#の作者はそのような喜びを私たちに提供しませんでした。まあ、それは問題ではありません。それにもかかわらず、それらは私たちが自分でこれを行うことを可能にしますが、それほど美しい方法ではありません。

まず、誰かがLINQとSystem.Collections.Generic.IEnumerableインターフェイスに共通点があり、個別に存在できないと言われた場合、安全に直接笑うことができます...

デザイン

	var query = 
		from itemA in listA
		join itemB in listB
			on itemA.Key equals itemB.Key
		select new {itemA, itemB};

コンパイラによって次の文字シーケンスに単純に変換されます。
	var query = listA.Join(listB, itemA => itemA.Key, itemB => itemB.Key, (itemA, itemB) => new {itemA, itemB});

また、listAとlistBがどのタイプの変数であるかは関係ありません。 listAがTypeA型の変数であり、永続的なitemBがTypeB型であるとします。したがって、TypeAとTypeBにKeyという名前のプロパティまたはフィールドが含まれている場合、TypeAには4つの引数を持つJoin()メソッドが含まれます。このLINQクエリは自由にコンパイルされます。

標準のIEnumerableインターフェイスを実装するLINQで変数を使用する場合、拡張メソッドが使用されます

public class System.Linq.Enumerable
{
		public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector) {...}
}

実際、このメソッドは有名なINNER JOINも生成します。そして今、ストリートマジックが始まります。LEFT / RIGHT / FULL OUTER JOIN(またはあなたの魂を喜ばせるJOIN)を実装するには、標準メソッドの呼び出しを、私たちが実装したものに置き換える必要があります。これを行うには、変数listAを何らかの方法で制御可能な型に変換する必要があります。

次の2つのクラスを実装することにより:

public class JoinedEnumerable<T> : IEnumerable<T>
{
	public readonly IEnumerable<T> Source;
	public bool IsOuter;

	public JoinedEnumerable(IEnumerable<T> source) { Source = source; }

	IEnumerator<T> IEnumerable<T>.GetEnumerator() { return Source.GetEnumerator(); }
	IEnumerator IEnumerable.GetEnumerator() { return Source.GetEnumerator(); }
}

public static class JoinedEnumerable
{
	public static JoinedEnumerable<TElement> Inner<TElement>(this IEnumerable<TElement> source)
	{
		return Wrap(source, false);
	}

	public static JoinedEnumerable<TElement> Outer<TElement>(this IEnumerable<TElement> source)
	{
		return Wrap(source, true);
	}

	public static JoinedEnumerable<TElement> Wrap(IEnumerable<TElement> source, bool isOuter)
	{
		JoinedEnumerable<TElement> joinedSource 
			= source as JoinedEnumerable<TElement> ?? 
				new JoinedEnumerable<TElement>(source);
		joinedSource.IsOuter = isOuter;
		return joinedSource;
	}
}

次のLINQクエリを簡単に作成できます

	var query = 
		from itemA in listA.Outer()
		join itemB in listB
			on itemA.Key equals itemB.Key
		select new {itemA, itemB};

そして、JoinedEnumerableクラスのJoin拡張メソッドを必要な方法で実装すると、必要なものがすべて取得されます。

そしてここに拡張メソッドがあります:

public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(this JoinedEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector, IEqualityComparer<TKey> comparer = null)
{
	if (outer == null) throw new ArgumentNullException("outer");
	if (inner == null) throw new ArgumentNullException("inner");
	if (outerKeySelector == null) throw new ArgumentNullException("outerKeySelector");
	if (innerKeySelector == null) throw new ArgumentNullException("innerKeySelector");
	if (resultSelector == null) throw new ArgumentNullException("resultSelector");

	bool leftOuter = outer.IsOuter;
	bool rightOuter = (inner is JoinedEnumerable<TInner>) && ((JoinedEnumerable<TInner>)inner).IsOuter;

	if (leftOuter && rightOuter)
		return FullOuterJoin(outer, inner, outerKeySelector, innerKeySelector, resultSelector, comparer);

	if (leftOuter)
		return LeftOuterJoin(outer, inner, outerKeySelector, innerKeySelector, resultSelector, comparer);

	if (rightOuter)
		return RightOuterJoin(outer, inner, outerKeySelector, innerKeySelector, resultSelector, comparer);

	return Enumerable.Join(outer, inner, outerKeySelector, innerKeySelector, resultSelector, comparer);
}

public static IEnumerable<TResult> LeftOuterJoin<TOuter, TInner, TKey, TResult>(this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector, IEqualityComparer<TKey> comparer = null)
{
	var innerLookup = inner.ToLookup(innerKeySelector, comparer);

	foreach (var outerItem in outer)
		foreach (var innerItem in innerLookup[outerKeySelector(outerItem)].DefaultIfEmpty())
			yield return resultSelector(outerItem, innerItem);
}

public static IEnumerable<TResult> RightOuterJoin<TOuter, TInner, TKey, TResult>(this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector, IEqualityComparer<TKey> comparer = null)
{
	var outerLookup = outer.ToLookup(outerKeySelector, comparer);

	foreach (var innerItem in inner)
		foreach (var outerItem in outerLookup[innerKeySelector(innerItem)].DefaultIfEmpty())
			yield return resultSelector(outerItem, innerItem);
}

public static IEnumerable<TResult> FullOuterJoin<TOuter, TInner, TKey, TResult>(this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector, IEqualityComparer<TKey> comparer = null)
{
	var outerLookup = outer.ToLookup(outerKeySelector, comparer);
	var innerLookup = inner.ToLookup(innerKeySelector, comparer);

	foreach (var innerGrouping in innerLookup)
		if (!outerLookup.Contains(innerGrouping.Key))
			foreach (TInner innerItem in innerGrouping)
				yield return resultSelector(default(TOuter), innerItem);

	foreach (var outerGrouping in outerLookup)
		foreach (var innerItem in innerLookup[outerGrouping.Key].DefaultIfEmpty())
			foreach (var outerItem in outerGrouping)
				yield return resultSelector(outerItem, innerItem);
}

Voila ...

美しいLEFT OUTER JOIN:

	var query = 
		from itemA in listA.Outer()
		join itemB in listB
			on itemA.Key equals itemB.Key
		select new {itemA, itemB};

美しい右外部結合:

	var query = 
		from itemA in listA.Inner()
		join itemB in listB.Outer()
			on itemA.Key equals itemB.Key
		select new {itemA, itemB};

美しいFULL OUTER JOIN:

	var query = 
		from itemA in listA.Outer()
		join itemB in listB.Outer()
			on itemA.Key equals itemB.Key
		select new {itemA, itemB};

ここで、必要に応じて、独自のアプローチを使用できます。想像力のフィールドがここにあるためです。zashashnikにグッズを実装するための興味深いソリューションがいくつかあります。それらを共有する時間があるでしょう。

ご清聴ありがとうございました。

POWERがあなたと一緒にいられるように!

All Articles