Junção externa na LINQ

LINQ - quanto foi inventado em C # apenas para que possamos desfrutar das delícias da Consulta Integrada à Linguagem. Nomeadamente:

  • Genéricos
  • Métodos de extensão
  • Expressões Lamda
  • Árvores de expressão
  • Tipos de Anonumus
  • Inicializadores de objeto
  • Tipo inferindo

E isso é tudo, para que possamos escrever algo como isto:
	var query = 
		from itemA in listA
		join itemB in listB
			on itemA.Key equals itemB.Key
		select new {itemA, itemB};
	

Não se pode discordar - impressiona.

E no meio de todo esse açúcar sintático, havia uma colher de orvalho que me impedia de dormir o suficiente :)

Essa é uma total falta de apoio para OUTER JOIN. Mas, como se viu, o alcatrão gira facilmente ... gira ... gira ...

... em outro açúcar sintático.

Aqueles que tentaram encontrar uma solução para o LEFT OUTER JOIN na Internet, provavelmente conhecem uma solução semelhante:

	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};
	

Esse projeto confunde claramente o entendimento por uma ordem de magnitude e complica uma construção já simples. E isso é apenas um substituto para INNER JOIN com LEFT OUTTER JOIN. Para não continuar chocando, não darei um exemplo com uma junção externa completa.

Parece que seria simples se pudéssemos escrever assim:

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

ou então

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

Mas não. Os autores de C # não nos proporcionaram tanto prazer. Bem, isso não importa. No entanto, eles nos permitirão fazer isso sozinhos, embora não de uma maneira tão bonita.

Para começar, se alguém lhe disser que o LINQ e a interface System.Collections.Generic.IEnumerable têm algo em comum e não podem existir separadamente, você pode rir com segurança pessoalmente ...

Design

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

é simplesmente traduzido pelo compilador na seguinte sequência de caracteres:
	var query = listA.Join(listB, itemA => itemA.Key, itemB => itemB.Key, (itemA, itemB) => new {itemA, itemB});

e não importa que tipo de variáveis ​​listA e listB sejam. Suponha que a lista A seja uma variável do tipo Tipo A e o item permanente B seja do tipo Tipo B. Portanto, se TypeA e TypeB contiverem uma propriedade ou campo chamado Key, TypeA conterá o método Join () com 4 argumentos. Essa consulta LINQ é compilada livremente.

Ao usar variáveis ​​no LINQ que implementam a interface IEnumerable padrão, o método de extensão é usado

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) {...}
}

Na verdade, esse método também produz o conhecido INNER JOIN. E agora a magia das ruas começa. Para implementar a JUNTA EXTERIOR ESQUERDA / DIREITA / CHEIA (ou JUNTA que agradará sua alma), é necessário substituir a chamada do método padrão pelo implementado por nós. Para fazer isso, precisamos converter a variável listA de alguma forma em um tipo que possamos controlar.

Implementando as duas classes a seguir:

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;
	}
}

podemos escrever facilmente a próxima consulta LINQ

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

e agora implementando o método de extensão Join para a classe JoinEnumerable da maneira que precisamos, obtemos tudo o que precisamos.

E aqui estão os métodos de extensão:

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 ...

Linda ESQUERDA JUNTA EXTERNA:

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

JOGO EXTERIOR DIREITO Bonito:

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

JOGO EXTERIOR COMPLETO:

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

Agora, se desejar, você pode usar sua própria abordagem - já que o campo da imaginação é enorme aqui. Eu tenho várias soluções mais interessantes para implementar brindes no zashashnik. Haverá tempo para compartilhá-los.

Obrigado pela atenção.

Que a POWER esteja com você!

All Articles