Unión externa en LINQ

LINQ: cuánto se ha inventado en C # solo para que podamos disfrutar de las delicias de Language Integrated Query. A saber:

  • Genéricos
  • Métodos de extensión
  • Expresiones Lamda
  • Árboles de expresión
  • Tipos de anonumus
  • Inicializadores de objeto
  • Escriba inferir

Y eso es todo para que podamos escribir algo como esto:
	var query = 
		from itemA in listA
		join itemB in listB
			on itemA.Key equals itemB.Key
		select new {itemA, itemB};
	

Uno no puede estar en desacuerdo, impresiona.

Y en medio de todo este azúcar sintáctico, había una cucharada de rocío que me impedía dormir lo suficiente :)

Esto es una falta total de apoyo para OUTER JOIN. Pero como resultó, el alquitrán gira fácilmente ... gira ... gira ...

... en otro azúcar sintáctico.

Aquellos que intentaron encontrar una solución para LEFT OUTER JOIN en Internet, probablemente conozcan una solución similar:

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

Tal diseño confunde claramente la comprensión por un orden de magnitud y complica una construcción ya simple. Y esto es solo un reemplazo para INNER JOIN con LEFT OUTER JOIN. Para no continuar conmocionando, no daré un ejemplo con una COMBINACIÓN EXTERNA COMPLETA.

Parecería que sería simple si pudiéramos escribir así:

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

más o menos

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

Pero no. Los autores de C # no nos proporcionaron tanto placer. Bueno, no importa. Sin embargo, nos permitirán hacer esto por nuestra cuenta, aunque no de una manera tan hermosa.

Para empezar, si alguien le dice que LINQ y la interfaz System.Collections.Generic.IEnumerable tienen algo en común y no pueden existir por separado, puede reírse con seguridad en persona ...

Diseño

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

simplemente es traducido por el compilador a la siguiente secuencia de caracteres:
	var query = listA.Join(listB, itemA => itemA.Key, itemB => itemB.Key, (itemA, itemB) => new {itemA, itemB});

y no importa qué tipo de variables sean la lista A y la lista B. Supongamos que listA es una variable de tipo TypeA, y que el elemento B permanente es de tipo TypeB. Entonces, si TypeA y TypeB contienen una propiedad o campo llamado Key, TypeA contiene el método Join () con 4 argumentos. Esta consulta LINQ se compila libremente.

Cuando se usan variables en LINQ que implementan la interfaz IEnumerable estándar, se usa el método de extensión

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

En realidad, este método también produce el conocido INNER JOIN. Y ahora comienza la magia de la calle. Para implementar la UNIÓN EXTERIOR IZQUIERDA / DERECHA / COMPLETA (o UNIR que complacerá a su alma), es necesario reemplazar la llamada del método estándar con el implementado por nosotros. Para hacer esto, necesitamos convertir la variable listA de alguna manera a un tipo que podamos controlar.

Al implementar las dos clases siguientes:

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 escribir fácilmente la 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};

y ahora implementando el método de extensión Join para la clase JoinedEnumerable de la manera que necesitamos, obtenemos todo lo que necesitamos.

Y aquí están los métodos de extensión:

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

Hermosa IZQUIERDA EXTERIOR ÚNETE:

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

Hermosa DERECHA EXTERIOR UNIRSE:

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

Hermosa UNIÓN COMPLETA EXTERIOR:

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

Ahora, si lo desea, puede usar su propio enfoque, ya que el campo para la imaginación es enorme aquí. Tengo varias soluciones más interesantes para implementar golosinas en el zashashnik. Habrá tiempo para compartirlos.

Gracias por la atención.

¡Que el PODER esté contigo!

All Articles