LINQ - Unión externa completa


204

Tengo una lista de identificación de la gente y su nombre, y una lista de identificación de la gente y su apellido. Algunas personas no tienen un nombre y otras no tienen un apellido; Me gustaría hacer una combinación externa completa en las dos listas.

Entonces las siguientes listas:

ID  FirstName
--  ---------
 1  John
 2  Sue

ID  LastName
--  --------
 1  Doe
 3  Smith

Debe producir:

ID  FirstName  LastName
--  ---------  --------
 1  John       Doe
 2  Sue
 3             Smith

Soy nuevo en LINQ (así que perdónenme si soy cojo) y he encontrado bastantes soluciones para 'LINQ Outer Joins', que parecen bastante similares, pero realmente parecen quedar unidas por fuera.

Mis intentos hasta ahora son algo como esto:

private void OuterJoinTest()
{
    List<FirstName> firstNames = new List<FirstName>();
    firstNames.Add(new FirstName { ID = 1, Name = "John" });
    firstNames.Add(new FirstName { ID = 2, Name = "Sue" });

    List<LastName> lastNames = new List<LastName>();
    lastNames.Add(new LastName { ID = 1, Name = "Doe" });
    lastNames.Add(new LastName { ID = 3, Name = "Smith" });

    var outerJoin = from first in firstNames
        join last in lastNames
        on first.ID equals last.ID
        into temp
        from last in temp.DefaultIfEmpty()
        select new
        {
            id = first != null ? first.ID : last.ID,
            firstname = first != null ? first.Name : string.Empty,
            surname = last != null ? last.Name : string.Empty
        };
    }
}

public class FirstName
{
    public int ID;

    public string Name;
}

public class LastName
{
    public int ID;

    public string Name;
}

Pero esto vuelve:

ID  FirstName  LastName
--  ---------  --------
 1  John       Doe
 2  Sue

¿Qué estoy haciendo mal?


2
¿Necesita que esto funcione solo para listas en memoria o para Linq2Sql?
JamesFaix

Respuestas:


123

No sé si esto cubre todos los casos, lógicamente parece correcto. La idea es tomar una unión externa izquierda y una unión externa derecha y luego tomar la unión de los resultados.

var firstNames = new[]
{
    new { ID = 1, Name = "John" },
    new { ID = 2, Name = "Sue" },
};
var lastNames = new[]
{
    new { ID = 1, Name = "Doe" },
    new { ID = 3, Name = "Smith" },
};
var leftOuterJoin =
    from first in firstNames
    join last in lastNames on first.ID equals last.ID into temp
    from last in temp.DefaultIfEmpty()
    select new
    {
        first.ID,
        FirstName = first.Name,
        LastName = last?.Name,
    };
var rightOuterJoin =
    from last in lastNames
    join first in firstNames on last.ID equals first.ID into temp
    from first in temp.DefaultIfEmpty()
    select new
    {
        last.ID,
        FirstName = first?.Name,
        LastName = last.Name,
    };
var fullOuterJoin = leftOuterJoin.Union(rightOuterJoin);

Esto funciona como está escrito, ya que está en LINQ to Objects. Si LINQ to SQL u otro, el procesador de consultas podría no admitir navegación segura u otras operaciones. Tendría que usar el operador condicional para obtener condicionalmente los valores.

es decir,

var leftOuterJoin =
    from first in firstNames
    join last in lastNames on first.ID equals last.ID into temp
    from last in temp.DefaultIfEmpty()
    select new
    {
        first.ID,
        FirstName = first.Name,
        LastName = last != null ? last.Name : default,
    };

2
La unión eliminará los duplicados. Si no espera duplicados, o puede escribir la segunda consulta para excluir todo lo que se incluyó en la primera, use Concat en su lugar. Esta es la diferencia SQL entre UNION y UNION ALL
cadrell0

3
Se producirán duplicados en @ cadre110 si una persona tiene un nombre y un apellido, por lo que la unión es una opción válida.
saus

1
@saus pero hay una columna de ID, por lo que incluso si hay un nombre y apellido duplicado, el ID debe ser diferente
cadrell0

1
Su solución funciona para tipos primitivos, pero no parece funcionar para objetos. En mi caso, FirstName es un objeto de dominio, mientras que LastName es otro objeto de dominio. Cuando uní los dos resultados, LINQ lanzó una excepción NotSupportedException (los tipos en Union o Concat se construyen de manera incompatible). ¿Has experimentado problemas similares?
Candy Chiu

1
@CandyChiu: En realidad nunca me encontré con un caso así. Supongo que es una limitación con su proveedor de consultas. Probablemente quiera usar LINQ to Objects en ese caso llamando AsEnumerable()antes de realizar la unión / concatenación. Pruebe eso y vea cómo va eso. Si esta no es la ruta que desea tomar, no estoy seguro de que pueda serle de más ayuda.
Jeff Mercado

196

Actualización 1: proporcionar un método de extensión verdaderamente generalizado FullOuterJoin
Actualización 2: opcionalmente aceptar un personalizado IEqualityComparerpara el tipo de clave
Actualización 3 : esta implementación se ha convertido recientemente en parte deMoreLinq - ¡Gracias chicos!

Editar agregado FullOuterGroupJoin( ideone ). Reutilicé la GetOuter<>implementación, haciendo que esta sea una fracción menos eficaz de lo que podría ser, pero estoy apuntando a un código de 'alto nivel', no optimizado, en este momento.

Véalo en vivo en http://ideone.com/O36nWc

static void Main(string[] args)
{
    var ax = new[] { 
        new { id = 1, name = "John" },
        new { id = 2, name = "Sue" } };
    var bx = new[] { 
        new { id = 1, surname = "Doe" },
        new { id = 3, surname = "Smith" } };

    ax.FullOuterJoin(bx, a => a.id, b => b.id, (a, b, id) => new {a, b})
        .ToList().ForEach(Console.WriteLine);
}

Imprime la salida:

{ a = { id = 1, name = John }, b = { id = 1, surname = Doe } }
{ a = { id = 2, name = Sue }, b =  }
{ a = , b = { id = 3, surname = Smith } }

También puede proporcionar valores predeterminados: http://ideone.com/kG4kqO

    ax.FullOuterJoin(
            bx, a => a.id, b => b.id, 
            (a, b, id) => new { a.name, b.surname },
            new { id = -1, name    = "(no firstname)" },
            new { id = -2, surname = "(no surname)" }
        )

Impresión:

{ name = John, surname = Doe }
{ name = Sue, surname = (no surname) }
{ name = (no firstname), surname = Smith }

Explicación de los términos utilizados:

Unirse es un término tomado del diseño de una base de datos relacional:

  • Una unión repetirá elementos atantas veces como haya elementos b con la clave correspondiente (es decir: nada si bestuviera vacío). La jerga de la base de datos llama a estoinner (equi)join .
  • Una unión externa incluye elementos apara los que no existe ningún elemento correspondienteb . (es decir: incluso los resultados si bestuvieran vacíos). Esto generalmente se conoce comoleft join .
  • Una combinación externa completa incluye registros de a , así comob si no existe ningún elemento correspondiente en el otro. (es decir, incluso los resultados si aestuvieran vacíos)

Algo que generalmente no se ve en RDBMS es una unión grupal [1] :

  • Una unión de grupo , hace lo mismo que se describió anteriormente, pero en lugar de repetir elementos de amúltiples correspondientes b, agrupa los registros con las teclas correspondientes. Esto suele ser más conveniente cuando desea enumerar a través de registros 'unidos', basados ​​en una clave común.

Consulte también GroupJoin, que también contiene algunas explicaciones generales de antecedentes.


[1] (Creo que Oracle y MSSQL tienen extensiones propietarias para esto)

Código completo

Una clase de extensión 'drop-in' generalizada para esto

internal static class MyExtensions
{
    internal static IEnumerable<TResult> FullOuterGroupJoin<TA, TB, TKey, TResult>(
        this IEnumerable<TA> a,
        IEnumerable<TB> b,
        Func<TA, TKey> selectKeyA, 
        Func<TB, TKey> selectKeyB,
        Func<IEnumerable<TA>, IEnumerable<TB>, TKey, TResult> projection,
        IEqualityComparer<TKey> cmp = null)
    {
        cmp = cmp?? EqualityComparer<TKey>.Default;
        var alookup = a.ToLookup(selectKeyA, cmp);
        var blookup = b.ToLookup(selectKeyB, cmp);

        var keys = new HashSet<TKey>(alookup.Select(p => p.Key), cmp);
        keys.UnionWith(blookup.Select(p => p.Key));

        var join = from key in keys
                   let xa = alookup[key]
                   let xb = blookup[key]
                   select projection(xa, xb, key);

        return join;
    }

    internal static IEnumerable<TResult> FullOuterJoin<TA, TB, TKey, TResult>(
        this IEnumerable<TA> a,
        IEnumerable<TB> b,
        Func<TA, TKey> selectKeyA, 
        Func<TB, TKey> selectKeyB,
        Func<TA, TB, TKey, TResult> projection,
        TA defaultA = default(TA), 
        TB defaultB = default(TB),
        IEqualityComparer<TKey> cmp = null)
    {
        cmp = cmp?? EqualityComparer<TKey>.Default;
        var alookup = a.ToLookup(selectKeyA, cmp);
        var blookup = b.ToLookup(selectKeyB, cmp);

        var keys = new HashSet<TKey>(alookup.Select(p => p.Key), cmp);
        keys.UnionWith(blookup.Select(p => p.Key));

        var join = from key in keys
                   from xa in alookup[key].DefaultIfEmpty(defaultA)
                   from xb in blookup[key].DefaultIfEmpty(defaultB)
                   select projection(xa, xb, key);

        return join;
    }
}

Editado para mostrar el uso del FullOuterJoinmétodo de extensión proporcionado
sehe

Editado: Método de extensión FullOuterGroupJoin agregado
sehe

44
En lugar de usar un Diccionario, puede usar una Búsqueda , que contiene la funcionalidad expresada en sus métodos de extensión auxiliar. Por ejemplo, puede escribir a.GroupBy(selectKeyA).ToDictionary();como a.ToLookup(selectKeyA)y adict.OuterGet(key)como alookup[key]. Sin embargo, obtener la colección de llaves es un poco más complicado:alookup.Select(x => x.Keys) .
Risky Martin

1
@RiskyMartin ¡Gracias! Eso, de hecho, hace que todo sea más elegante. Actualicé la respuestaActualicé y las ideone-s. (Supongo que el rendimiento debería aumentarse ya que se crean instancias de menos objetos).
sehe

1
@Revious eso solo funciona si sabes que las claves son únicas. Y ese no es el caso común para / agrupación /. Aparte de eso, sí, por supuesto. Si sabe que el hash no va a arrastrar el rendimiento (los contenedores basados ​​en nodos tienen más costos en principio, y el hash no es gratuito y la eficiencia depende de la función de hash / distribución del depósito), sin duda será más eficiente algorítmicamente. Por lo tanto, para cargas pequeñas, esperaría que no sea más rápido
sehe

27

Creo que hay problemas con la mayoría de estos, incluida la respuesta aceptada, porque no funcionan bien con Linq sobre IQueryable, ya sea por hacer demasiados viajes de ida y vuelta al servidor y demasiados retornos de datos, o por hacer demasiada ejecución del cliente.

Para IEnumerable no me gusta la respuesta de Sehe o similar porque tiene un uso excesivo de memoria (una simple prueba 10000000 de dos listas ejecutó a Linqpad sin memoria en mi máquina de 32 GB).

Además, la mayoría de los demás en realidad no implementan una unión externa completa adecuada porque están utilizando una unión con una unión derecha en lugar de una concat con una unión anti derecha semi, que no solo elimina las filas duplicadas de unión interna del resultado, sino que también cualquier duplicado adecuado que existiera originalmente en los datos izquierdo o derecho.

Así que aquí están mis extensiones que manejan todos estos problemas, generan SQL e implementan la unión en LINQ to SQL directamente, ejecutándose en el servidor, y es más rápido y con menos memoria que otros en Enumerables:

public static class Ext {
    public static IEnumerable<TResult> LeftOuterJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector) {

        return from left in leftItems
               join right in rightItems on leftKeySelector(left) equals rightKeySelector(right) into temp
               from right in temp.DefaultIfEmpty()
               select resultSelector(left, right);
    }

    public static IEnumerable<TResult> RightOuterJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector) {

        return from right in rightItems
               join left in leftItems on rightKeySelector(right) equals leftKeySelector(left) into temp
               from left in temp.DefaultIfEmpty()
               select resultSelector(left, right);
    }

    public static IEnumerable<TResult> FullOuterJoinDistinct<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector) {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Union(leftItems.RightOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }

    public static IEnumerable<TResult> RightAntiSemiJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector) {

        var hashLK = new HashSet<TKey>(from l in leftItems select leftKeySelector(l));
        return rightItems.Where(r => !hashLK.Contains(rightKeySelector(r))).Select(r => resultSelector(default(TLeft),r));
    }

    public static IEnumerable<TResult> FullOuterJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> leftItems,
        IEnumerable<TRight> rightItems,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector)  where TLeft : class {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Concat(leftItems.RightAntiSemiJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }

    private static Expression<Func<TP, TC, TResult>> CastSMBody<TP, TC, TResult>(LambdaExpression ex, TP unusedP, TC unusedC, TResult unusedRes) => (Expression<Func<TP, TC, TResult>>)ex;

    public static IQueryable<TResult> LeftOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        var sampleAnonLR = new { left = default(TLeft), rightg = default(IEnumerable<TRight>) };
        var parmP = Expression.Parameter(sampleAnonLR.GetType(), "p");
        var parmC = Expression.Parameter(typeof(TRight), "c");
        var argLeft = Expression.PropertyOrField(parmP, "left");
        var newleftrs = CastSMBody(Expression.Lambda(Expression.Invoke(resultSelector, argLeft, parmC), parmP, parmC), sampleAnonLR, default(TRight), default(TResult));

        return leftItems.AsQueryable().GroupJoin(rightItems, leftKeySelector, rightKeySelector, (left, rightg) => new { left, rightg }).SelectMany(r => r.rightg.DefaultIfEmpty(), newleftrs);
    }

    public static IQueryable<TResult> RightOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        var sampleAnonLR = new { leftg = default(IEnumerable<TLeft>), right = default(TRight) };
        var parmP = Expression.Parameter(sampleAnonLR.GetType(), "p");
        var parmC = Expression.Parameter(typeof(TLeft), "c");
        var argRight = Expression.PropertyOrField(parmP, "right");
        var newrightrs = CastSMBody(Expression.Lambda(Expression.Invoke(resultSelector, parmC, argRight), parmP, parmC), sampleAnonLR, default(TLeft), default(TResult));

        return rightItems.GroupJoin(leftItems, rightKeySelector, leftKeySelector, (right, leftg) => new { leftg, right }).SelectMany(l => l.leftg.DefaultIfEmpty(), newrightrs);
    }

    public static IQueryable<TResult> FullOuterJoinDistinct<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Union(leftItems.RightOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }

    private static Expression<Func<TP, TResult>> CastSBody<TP, TResult>(LambdaExpression ex, TP unusedP, TResult unusedRes) => (Expression<Func<TP, TResult>>)ex;

    public static IQueryable<TResult> RightAntiSemiJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        var sampleAnonLgR = new { leftg = default(IEnumerable<TLeft>), right = default(TRight) };
        var parmLgR = Expression.Parameter(sampleAnonLgR.GetType(), "lgr");
        var argLeft = Expression.Constant(default(TLeft), typeof(TLeft));
        var argRight = Expression.PropertyOrField(parmLgR, "right");
        var newrightrs = CastSBody(Expression.Lambda(Expression.Invoke(resultSelector, argLeft, argRight), parmLgR), sampleAnonLgR, default(TResult));

        return rightItems.GroupJoin(leftItems, rightKeySelector, leftKeySelector, (right, leftg) => new { leftg, right }).Where(lgr => !lgr.leftg.Any()).Select(newrightrs);
    }

    public static IQueryable<TResult> FullOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Concat(leftItems.RightAntiSemiJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }
}

La diferencia entre un Anti-Semi-Join correcto es principalmente discutible con Linq to Objects o en la fuente, pero hace una diferencia en el lado del servidor (SQL) en la respuesta final, eliminando un innecesario JOIN .

La codificación manual de Expression manejar la fusión de un Expression<Func<>>en un lambda podría mejorarse con LinqKit, pero sería bueno si el lenguaje / compilador hubiera agregado alguna ayuda para eso. Las funciones FullOuterJoinDistincty RightOuterJoinse incluyen para completar, pero no volví a implementarFullOuterGroupJoin .

Escribí otra versión de una combinación externa completa paraIEnumerable para los casos en que la clave es ordenable, que es aproximadamente un 50% más rápida que la combinación de la combinación externa izquierda con la combinación anti derecha, al menos en pequeñas colecciones. Revisa cada colección después de ordenar solo una vez.

También agregué otra respuesta para una versión que funciona con EF al reemplazarla Invokecon una expansión personalizada.


¿De qué se trata TP unusedP, TC unusedC? ¿Están literalmente sin usar?
Rudey

Sí, ellos sólo están presentes para capturar los tipos de TP, TC, TResultpara crear el correcto Expression<Func<>>. Supuse que podría reemplazarlos con _, __, ___en su lugar, pero que no parece más claro hasta que C # tiene un comodín parámetro adecuado para usar en su lugar.
NetMage

1
@MarcL. No estoy tan seguro acerca de 'cansado', pero estoy de acuerdo en que esta respuesta es muy útil en este contexto. Cosas impresionantes (aunque para mí confirma las deficiencias de Linq-to-SQL)
sehe

3
Me estoy poniendo The LINQ expression node type 'Invoke' is not supported in LINQ to Entities.. ¿Hay alguna restricción con este código? Quiero realizar una UNIÓN COMPLETA sobre IQueryables
Estudiante

1
He agregado una nueva respuesta que reemplaza Invokecon una personalizada ExpressionVisitorpara alinearla, Invokepor lo que debería funcionar con EF. ¿Puedes probarlo?
NetMage

7

Aquí hay un método de extensión que hace eso:

public static IEnumerable<KeyValuePair<TLeft, TRight>> FullOuterJoin<TLeft, TRight>(this IEnumerable<TLeft> leftItems, Func<TLeft, object> leftIdSelector, IEnumerable<TRight> rightItems, Func<TRight, object> rightIdSelector)
{
    var leftOuterJoin = from left in leftItems
        join right in rightItems on leftIdSelector(left) equals rightIdSelector(right) into temp
        from right in temp.DefaultIfEmpty()
        select new { left, right };

    var rightOuterJoin = from right in rightItems
        join left in leftItems on rightIdSelector(right) equals leftIdSelector(left) into temp
        from left in temp.DefaultIfEmpty()
        select new { left, right };

    var fullOuterJoin = leftOuterJoin.Union(rightOuterJoin);

    return fullOuterJoin.Select(x => new KeyValuePair<TLeft, TRight>(x.left, x.right));
}

3
+1. R ⟗ S = (R ⟕ S) ∪ (R ⟖ S), lo que significa una unión externa completa = unión de unión externa izquierda ¡unión externa derecha! Aprecio la simplicidad de este enfoque.
TamusJRoyce

1
@TamusJRoyce Except Unionelimina los duplicados, por lo que si hay filas duplicadas en los datos originales, no estarán en el resultado.
NetMage

Gran punto! agregue una identificación única si necesita evitar que se eliminen los duplicados. Si. La unión es un poco inútil, a menos que pueda insinuar que hay una identificación única y la unión cambia a la unión de todos (a través de optimizaciones / heurísticas internas). Pero funcionará.
TamusJRoyce


7

Supongo que el enfoque de @ sehe es más fuerte, pero hasta que lo entiendo mejor, me encuentro saltando de la extensión de @ MichaelSander. Lo modifiqué para que coincida con la sintaxis y el tipo de retorno del método incorporado Enumerable.Join () descrito aquí . Agregué el sufijo "distinto" con respecto al comentario de @ cadrell0 bajo la solución de @ JeffMercado.

public static class MyExtensions {

    public static IEnumerable<TResult> FullJoinDistinct<TLeft, TRight, TKey, TResult> (
        this IEnumerable<TLeft> leftItems, 
        IEnumerable<TRight> rightItems, 
        Func<TLeft, TKey> leftKeySelector, 
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TResult> resultSelector
    ) {

        var leftJoin = 
            from left in leftItems
            join right in rightItems 
              on leftKeySelector(left) equals rightKeySelector(right) into temp
            from right in temp.DefaultIfEmpty()
            select resultSelector(left, right);

        var rightJoin = 
            from right in rightItems
            join left in leftItems 
              on rightKeySelector(right) equals leftKeySelector(left) into temp
            from left in temp.DefaultIfEmpty()
            select resultSelector(left, right);

        return leftJoin.Union(rightJoin);
    }

}

En el ejemplo, lo usarías así:

var test = 
    firstNames
    .FullJoinDistinct(
        lastNames,
        f=> f.ID,
        j=> j.ID,
        (f,j)=> new {
            ID = f == null ? j.ID : f.ID, 
            leftName = f == null ? null : f.Name,
            rightName = j == null ? null : j.Name
        }
    );

En el futuro, a medida que aprenda más, tengo la sensación de que migraré a la lógica de @ sehe dada su popularidad. Pero incluso entonces tendré que tener cuidado, porque creo que es importante tener al menos una sobrecarga que coincida con la sintaxis del método ".Join ()" existente si es posible, por dos razones:

  1. La consistencia en los métodos ayuda a ahorrar tiempo, evitar errores y evitar comportamientos no deseados.
  2. Si alguna vez hay un método ".FullJoin ()" listo para usar en el futuro, me imagino que intentará mantener la sintaxis del método ".Join ()" actualmente existente si es posible. Si es así, si desea migrar a él, simplemente puede cambiar el nombre de sus funciones sin cambiar los parámetros o preocuparse por los diferentes tipos de retorno que rompen su código.

Todavía soy nuevo con genéricos, extensiones, declaraciones Func y otras características, por lo que los comentarios son bienvenidos.

EDITAR: No me llevó mucho tiempo darme cuenta de que había un problema con mi código. Estaba haciendo un .Dump () en LINQPad y mirando el tipo de retorno. Era simplemente IEnumerable, así que traté de igualarlo. Pero cuando realmente hice un .Where () o .Select () en mi extensión, recibí un error: "'System Collections.IEnumerable' no contiene una definición para 'Seleccionar' y ...". Así que al final pude hacer coincidir la sintaxis de entrada de .Join (), pero no el comportamiento de retorno.

EDITAR: Se agregó "TResult" al tipo de retorno para la función. Perdí eso al leer el artículo de Microsoft, y por supuesto tiene sentido. Con esta solución, ahora parece que el comportamiento de retorno está en línea con mis objetivos después de todo.


+2 por esta respuesta, así como por Michael Sanders. Accidentalmente hice clic en esto y la votación está bloqueada. Por favor agregue dos.
TamusJRoyce

@TamusJRoyce, acabo de editar un poco los formatos de código. Creo que después de realizar una edición, tiene la opción de volver a emitir su voto. Dale una oportunidad si quieres.
pwilcox

Muchas gracias!
Roshna Omer el

6

Como has encontrado, Linq no tiene una construcción de "unión externa". Lo más cercano que puede obtener es una combinación externa izquierda utilizando la consulta que indicó. Para esto, puede agregar cualquier elemento de la lista de apellidos que no esté representado en la unión:

outerJoin = outerJoin.Concat(lastNames.Select(l=>new
                            {
                                id = l.ID,
                                firstname = String.Empty,
                                surname = l.Name
                            }).Where(l=>!outerJoin.Any(o=>o.id == l.id)));

2

Me gusta la respuesta de sehe, pero no utiliza la ejecución diferida (las llamadas a ToLookup enumeran con entusiasmo las secuencias de entrada). Entonces, después de mirar las fuentes .NET para LINQ-to-objects , se me ocurrió esto:

public static class LinqExtensions
{
    public static IEnumerable<TResult> FullOuterJoin<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> left,
        IEnumerable<TRight> right,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TKey, TResult> resultSelector,
        IEqualityComparer<TKey> comparator = null,
        TLeft defaultLeft = default(TLeft),
        TRight defaultRight = default(TRight))
    {
        if (left == null) throw new ArgumentNullException("left");
        if (right == null) throw new ArgumentNullException("right");
        if (leftKeySelector == null) throw new ArgumentNullException("leftKeySelector");
        if (rightKeySelector == null) throw new ArgumentNullException("rightKeySelector");
        if (resultSelector == null) throw new ArgumentNullException("resultSelector");

        comparator = comparator ?? EqualityComparer<TKey>.Default;
        return FullOuterJoinIterator(left, right, leftKeySelector, rightKeySelector, resultSelector, comparator, defaultLeft, defaultRight);
    }

    internal static IEnumerable<TResult> FullOuterJoinIterator<TLeft, TRight, TKey, TResult>(
        this IEnumerable<TLeft> left,
        IEnumerable<TRight> right,
        Func<TLeft, TKey> leftKeySelector,
        Func<TRight, TKey> rightKeySelector,
        Func<TLeft, TRight, TKey, TResult> resultSelector,
        IEqualityComparer<TKey> comparator,
        TLeft defaultLeft,
        TRight defaultRight)
    {
        var leftLookup = left.ToLookup(leftKeySelector, comparator);
        var rightLookup = right.ToLookup(rightKeySelector, comparator);
        var keys = leftLookup.Select(g => g.Key).Union(rightLookup.Select(g => g.Key), comparator);

        foreach (var key in keys)
            foreach (var leftValue in leftLookup[key].DefaultIfEmpty(defaultLeft))
                foreach (var rightValue in rightLookup[key].DefaultIfEmpty(defaultRight))
                    yield return resultSelector(leftValue, rightValue, key);
    }
}

Esta implementación tiene las siguientes propiedades importantes:

  • Ejecución diferida, las secuencias de entrada no se enumerarán antes de que se enumere la secuencia de salida.
  • Solo enumera las secuencias de entrada una vez cada una.
  • Conserva el orden de las secuencias de entrada, en el sentido de que producirá tuplas en el orden de la secuencia izquierda y luego la derecha (para las teclas que no están presentes en la secuencia izquierda).

Estas propiedades son importantes, porque son lo que alguien nuevo en FullOuterJoin pero experimentado con LINQ esperará.


No conserva el orden de las secuencias de entrada: la búsqueda no garantiza eso, por lo que estos foreaches se enumerarán en algún orden del lado izquierdo, luego, algún orden del lado derecho no está presente en el lado izquierdo. Pero el orden relacional de los elementos no se conserva.
Ivan Danilov

@IvanDanilov Tienes razón en que esto no está realmente en el contrato. Sin embargo, la implementación de ToLookup usa una clase de búsqueda interna en Enumerable.cs que mantiene las agrupaciones en una lista vinculada ordenada por inserción y usa esta lista para recorrerlas en iteración. Entonces, en la versión actual de .NET, el orden está garantizado, pero dado que MS desafortunadamente no ha documentado esto, podrían cambiarlo en versiones posteriores.
Søren Boisen

Lo probé en .NET 4.5.1 en Win 8.1, y no conserva el orden.
Ivan Danilov

1
"..las secuencias de entrada se enumeran ansiosamente por las llamadas a ToLookup". Pero su implementación hace exactamente lo mismo. Ceder no da mucho aquí debido a los gastos en la máquina de estado finito.
pkuderov

44
Las llamadas de búsqueda se realizan cuando se solicita el primer elemento del resultado, y no cuando se crea el iterador. Eso es lo que significa ejecución diferida. Puede aplazar aún más la enumeración de un conjunto de entrada, iterando el Enumerable izquierdo directamente en lugar de convertirlo en una Búsqueda, lo que resulta en el beneficio adicional de que se conserva el orden del conjunto izquierdo.
Rolf

2

Decidí agregar esto como una respuesta separada, ya que no estoy seguro de que se haya probado lo suficiente. Esta es una re-implementación del FullOuterJoinmétodo usando esencialmente una versión simplificada y personalizada de LINQKit Invoke/ Expandfor Expressionpara que funcione el Entity Framework. No hay mucha explicación, ya que es más o menos lo mismo que mi respuesta anterior.

public static class Ext {
    private static Expression<Func<TP, TC, TResult>> CastSMBody<TP, TC, TResult>(LambdaExpression ex, TP unusedP, TC unusedC, TResult unusedRes) => (Expression<Func<TP, TC, TResult>>)ex;

    public static IQueryable<TResult> LeftOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        // (lrg,r) => resultSelector(lrg.left, r)
        var sampleAnonLR = new { left = default(TLeft), rightg = default(IEnumerable<TRight>) };
        var parmP = Expression.Parameter(sampleAnonLR.GetType(), "lrg");
        var parmC = Expression.Parameter(typeof(TRight), "r");
        var argLeft = Expression.PropertyOrField(parmP, "left");
        var newleftrs = CastSMBody(Expression.Lambda(resultSelector.Apply(argLeft, parmC), parmP, parmC), sampleAnonLR, default(TRight), default(TResult));

        return leftItems.GroupJoin(rightItems, leftKeySelector, rightKeySelector, (left, rightg) => new { left, rightg }).SelectMany(r => r.rightg.DefaultIfEmpty(), newleftrs);
    }

    public static IQueryable<TResult> RightOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) {

        // (lgr,l) => resultSelector(l, lgr.right)
        var sampleAnonLR = new { leftg = default(IEnumerable<TLeft>), right = default(TRight) };
        var parmP = Expression.Parameter(sampleAnonLR.GetType(), "lgr");
        var parmC = Expression.Parameter(typeof(TLeft), "l");
        var argRight = Expression.PropertyOrField(parmP, "right");
        var newrightrs = CastSMBody(Expression.Lambda(resultSelector.Apply(parmC, argRight), parmP, parmC), sampleAnonLR, default(TLeft), default(TResult));

        return rightItems.GroupJoin(leftItems, rightKeySelector, leftKeySelector, (right, leftg) => new { leftg, right })
                         .SelectMany(l => l.leftg.DefaultIfEmpty(), newrightrs);
    }

    private static Expression<Func<TParm, TResult>> CastSBody<TParm, TResult>(LambdaExpression ex, TParm unusedP, TResult unusedRes) => (Expression<Func<TParm, TResult>>)ex;

    public static IQueryable<TResult> RightAntiSemiJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector) where TLeft : class where TRight : class where TResult : class {

        // newrightrs = lgr => resultSelector(default(TLeft), lgr.right)
        var sampleAnonLgR = new { leftg = (IEnumerable<TLeft>)null, right = default(TRight) };
        var parmLgR = Expression.Parameter(sampleAnonLgR.GetType(), "lgr");
        var argLeft = Expression.Constant(default(TLeft), typeof(TLeft));
        var argRight = Expression.PropertyOrField(parmLgR, "right");
        var newrightrs = CastSBody(Expression.Lambda(resultSelector.Apply(argLeft, argRight), parmLgR), sampleAnonLgR, default(TResult));

        return rightItems.GroupJoin(leftItems, rightKeySelector, leftKeySelector, (right, leftg) => new { leftg, right }).Where(lgr => !lgr.leftg.Any()).Select(newrightrs);
    }

    public static IQueryable<TResult> FullOuterJoin<TLeft, TRight, TKey, TResult>(
        this IQueryable<TLeft> leftItems,
        IQueryable<TRight> rightItems,
        Expression<Func<TLeft, TKey>> leftKeySelector,
        Expression<Func<TRight, TKey>> rightKeySelector,
        Expression<Func<TLeft, TRight, TResult>> resultSelector)  where TLeft : class where TRight : class where TResult : class {

        return leftItems.LeftOuterJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector).Concat(leftItems.RightAntiSemiJoin(rightItems, leftKeySelector, rightKeySelector, resultSelector));
    }

    public static Expression Apply(this LambdaExpression e, params Expression[] args) {
        var b = e.Body;

        foreach (var pa in e.Parameters.Cast<ParameterExpression>().Zip(args, (p, a) => (p, a))) {
            b = b.Replace(pa.p, pa.a);
        }

        return b.PropagateNull();
    }

    public static Expression Replace(this Expression orig, Expression from, Expression to) => new ReplaceVisitor(from, to).Visit(orig);
    public class ReplaceVisitor : System.Linq.Expressions.ExpressionVisitor {
        public readonly Expression from;
        public readonly Expression to;

        public ReplaceVisitor(Expression _from, Expression _to) {
            from = _from;
            to = _to;
        }

        public override Expression Visit(Expression node) => node == from ? to : base.Visit(node);
    }

    public static Expression PropagateNull(this Expression orig) => new NullVisitor().Visit(orig);
    public class NullVisitor : System.Linq.Expressions.ExpressionVisitor {
        public override Expression Visit(Expression node) {
            if (node is MemberExpression nme && nme.Expression is ConstantExpression nce && nce.Value == null)
                return Expression.Constant(null, nce.Type.GetMember(nme.Member.Name).Single().GetMemberType());
            else
                return base.Visit(node);
        }
    }

    public static Type GetMemberType(this MemberInfo member) {
        switch (member) {
            case FieldInfo mfi:
                return mfi.FieldType;
            case PropertyInfo mpi:
                return mpi.PropertyType;
            case EventInfo mei:
                return mei.EventHandlerType;
            default:
                throw new ArgumentException("MemberInfo must be if type FieldInfo, PropertyInfo or EventInfo", nameof(member));
        }
    }
}

NetMage, codificación impresionante! Cuando lo ejecuto con un ejemplo simple, y cuando el [NullVisitor.Visit (..) se invoca en [base.Visit (Node)], arroja una [System.ArgumentException: los tipos de argumento no coinciden]. Lo cual es cierto, ya que estoy usando una [Guid] TKey y en algún momento el visitante nulo espera un tipo [Guid?]. Puede ser que me falta algo. Tengo un breve ejemplo codificado para EF 6.4.4. Avíseme cómo puedo compartir este código con usted. ¡Gracias!
Troncho

@Troncho Normalmente uso LINQPad para las pruebas, por lo que EF 6 no se hace fácilmente. base.Visit(node)No debería lanzar una excepción, ya que solo se repite en el árbol. Puedo acceder a casi cualquier servicio de código compartido, pero no configurar una base de datos de prueba. Sin embargo, ejecutarlo contra mi prueba LINQ to SQL parece funcionar bien.
NetMage

@Troncho ¿Es posible que se esté uniendo entre una Guidclave y una Guid?clave externa?
NetMage

Estoy usando LinqPad para probar también. Mi consulta arrojó la excepción ArgumentException, así que decidí depurarla en VS2019 en [.Net Framework 4.7.1] y el último EF 6. Allí pude rastrear el problema real. Para probar su código, estoy generando 2 conjuntos de datos separados que se originan en la misma tabla [Personas]. Filtro ambos conjuntos para que algunos registros sean únicos para cada conjunto y algunos existan en ambos conjuntos. [PersonId] es un Guid [Clave primaria] (c #) / Uniqueidentifier (SqlServer) y ninguno de los conjuntos genera ningún valor nulo [PersonId]. Código compartido: github.com/Troncho/EF_FullOuterJoin
Troncho

1

Realiza una enumeración de transmisión en memoria en ambas entradas e invoca el selector para cada fila. Si no hay correlación en la iteración actual, uno de los argumentos del selector será nulo .

Ejemplo:

   var result = left.FullOuterJoin(
         right, 
         x=>left.Key, 
         x=>right.Key, 
         (l,r) => new { LeftKey = l?.Key, RightKey=r?.Key });
  • Requiere un IComparer para el tipo de correlación, utiliza el Comparer.Default si no se proporciona.

  • Requiere que 'OrderBy' se aplique a los enumerables de entrada

    /// <summary>
    /// Performs a full outer join on two <see cref="IEnumerable{T}" />.
    /// </summary>
    /// <typeparam name="TLeft"></typeparam>
    /// <typeparam name="TValue"></typeparam>
    /// <typeparam name="TRight"></typeparam>
    /// <typeparam name="TResult"></typeparam>
    /// <param name="left"></param>
    /// <param name="right"></param>
    /// <param name="leftKeySelector"></param>
    /// <param name="rightKeySelector"></param>
    /// <param name="selector">Expression defining result type</param>
    /// <param name="keyComparer">A comparer if there is no default for the type</param>
    /// <returns></returns>
    [System.Diagnostics.DebuggerStepThrough]
    public static IEnumerable<TResult> FullOuterJoin<TLeft, TRight, TValue, TResult>(
        this IEnumerable<TLeft> left,
        IEnumerable<TRight> right,
        Func<TLeft, TValue> leftKeySelector,
        Func<TRight, TValue> rightKeySelector,
        Func<TLeft, TRight, TResult> selector,
        IComparer<TValue> keyComparer = null)
        where TLeft: class
        where TRight: class
        where TValue : IComparable
    {
    
        keyComparer = keyComparer ?? Comparer<TValue>.Default;
    
        using (var enumLeft = left.OrderBy(leftKeySelector).GetEnumerator())
        using (var enumRight = right.OrderBy(rightKeySelector).GetEnumerator())
        {
    
            var hasLeft = enumLeft.MoveNext();
            var hasRight = enumRight.MoveNext();
            while (hasLeft || hasRight)
            {
    
                var currentLeft = enumLeft.Current;
                var valueLeft = hasLeft ? leftKeySelector(currentLeft) : default(TValue);
    
                var currentRight = enumRight.Current;
                var valueRight = hasRight ? rightKeySelector(currentRight) : default(TValue);
    
                int compare =
                    !hasLeft ? 1
                    : !hasRight ? -1
                    : keyComparer.Compare(valueLeft, valueRight);
    
                switch (compare)
                {
                    case 0:
                        // The selector matches. An inner join is achieved
                        yield return selector(currentLeft, currentRight);
                        hasLeft = enumLeft.MoveNext();
                        hasRight = enumRight.MoveNext();
                        break;
                    case -1:
                        yield return selector(currentLeft, default(TRight));
                        hasLeft = enumLeft.MoveNext();
                        break;
                    case 1:
                        yield return selector(default(TLeft), currentRight);
                        hasRight = enumRight.MoveNext();
                        break;
                }
            }
    
        }
    
    }

1
Es un esfuerzo heroico para hacer que las cosas "se transmitan". Lamentablemente, toda la ganancia se pierde en el primer paso, donde se realiza OrderByen ambas proyecciones clave. OrderByamortigua toda la secuencia, por las razones obvias .
sehe

@sehe Definitivamente tienes razón para Linq to Objects. Si IEnumerable <T> son IQueryable <T>, la fuente debería ordenar, aunque no hay tiempo para probar. Si estoy equivocado sobre esto, simplemente reemplazando la entrada IEnumerable <T> con IQueryable <T> debería ordenar en la fuente / base de datos.
James Caradoc-Davies

1

Mi solución limpia para la situación de esa clave es única en ambos enumerables:

 private static IEnumerable<TResult> FullOuterJoin<Ta, Tb, TKey, TResult>(
            IEnumerable<Ta> a, IEnumerable<Tb> b,
            Func<Ta, TKey> key_a, Func<Tb, TKey> key_b,
            Func<Ta, Tb, TResult> selector)
        {
            var alookup = a.ToLookup(key_a);
            var blookup = b.ToLookup(key_b);
            var keys = new HashSet<TKey>(alookup.Select(p => p.Key));
            keys.UnionWith(blookup.Select(p => p.Key));
            return keys.Select(key => selector(alookup[key].FirstOrDefault(), blookup[key].FirstOrDefault()));
        }

entonces

    var ax = new[] {
        new { id = 1, first_name = "ali" },
        new { id = 2, first_name = "mohammad" } };
    var bx = new[] {
        new { id = 1, last_name = "rezaei" },
        new { id = 3, last_name = "kazemi" } };

    var list = FullOuterJoin(ax, bx, a => a.id, b => b.id, (a, b) => "f: " + a?.first_name + " l: " + b?.last_name).ToArray();

salidas:

f: ali l: rezaei
f: mohammad l:
f:  l: kazemi

0

Unión externa completa para dos o más tablas: primero extraiga la columna a la que desea unir.

var DatesA = from A in db.T1 select A.Date; 
var DatesB = from B in db.T2 select B.Date; 
var DatesC = from C in db.T3 select C.Date;            

var Dates = DatesA.Union(DatesB).Union(DatesC); 

Luego use la unión externa izquierda entre la columna extraída y las tablas principales.

var Full_Outer_Join =

(from A in Dates
join B in db.T1
on A equals B.Date into AB 

from ab in AB.DefaultIfEmpty()
join C in db.T2
on A equals C.Date into ABC 

from abc in ABC.DefaultIfEmpty()
join D in db.T3
on A equals D.Date into ABCD

from abcd in ABCD.DefaultIfEmpty() 
select new { A, ab, abc, abcd })
.AsEnumerable();

0

Escribí esta clase de extensiones para una aplicación hace aproximadamente 6 años, y la he estado usando desde entonces en muchas soluciones sin problemas. Espero eso ayude.

editar: noté que algunos podrían no saber cómo usar una clase de extensión.

Para usar esta clase de extensión, solo haga referencia a su espacio de nombres en su clase agregando la siguiente línea usando joinext;

^ esto debería permitirle ver la inteligencia de las funciones de extensión en cualquier colección de objetos IEnumerable que utilice.

Espero que esto ayude. Avíseme si aún no está claro, y espero escribir un ejemplo de ejemplo sobre cómo usarlo.

Ahora aquí está la clase:

namespace joinext
{    
public static class JoinExtensions
    {
        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)
            where TInner : class
            where TOuter : class
        {
            var innerLookup = inner.ToLookup(innerKeySelector);
            var outerLookup = outer.ToLookup(outerKeySelector);

            var innerJoinItems = inner
                .Where(innerItem => !outerLookup.Contains(innerKeySelector(innerItem)))
                .Select(innerItem => resultSelector(null, innerItem));

            return outer
                .SelectMany(outerItem =>
                {
                    var innerItems = innerLookup[outerKeySelector(outerItem)];

                    return innerItems.Any() ? innerItems : new TInner[] { null };
                }, resultSelector)
                .Concat(innerJoinItems);
        }


        public static IEnumerable<TResult> LeftJoin<TOuter, TInner, TKey, TResult>(
            this IEnumerable<TOuter> outer,
            IEnumerable<TInner> inner,
            Func<TOuter, TKey> outerKeySelector,
            Func<TInner, TKey> innerKeySelector,
            Func<TOuter, TInner, TResult> resultSelector)
        {
            return outer.GroupJoin(
                inner,
                outerKeySelector,
                innerKeySelector,
                (o, i) =>
                    new { o = o, i = i.DefaultIfEmpty() })
                    .SelectMany(m => m.i.Select(inn =>
                        resultSelector(m.o, inn)
                        ));

        }



        public static IEnumerable<TResult> RightJoin<TOuter, TInner, TKey, TResult>(
            this IEnumerable<TOuter> outer,
            IEnumerable<TInner> inner,
            Func<TOuter, TKey> outerKeySelector,
            Func<TInner, TKey> innerKeySelector,
            Func<TOuter, TInner, TResult> resultSelector)
        {
            return inner.GroupJoin(
                outer,
                innerKeySelector,
                outerKeySelector,
                (i, o) =>
                    new { i = i, o = o.DefaultIfEmpty() })
                    .SelectMany(m => m.o.Select(outt =>
                        resultSelector(outt, m.i)
                        ));

        }

    }
}

1
Desafortunadamente, parece que la función SelectManyno se puede convertir en un árbol de expresión digno de LINQ2SQL.
O Mapper

edc65. Sé que podría ser una pregunta tonta si ya lo hiciste. Pero por si acaso (como he notado que algunos no saben), solo necesita hacer referencia al espacio de nombres joinext.
H7O

O Mapper, hágame saber con qué tipo de colección desea que funcione. Debería funcionar bien con cualquier colección
IEnumerable

0

Creo que la cláusula de unión de LINQ no es la solución correcta para este problema, porque el propósito de la cláusula de unión no es acumular datos de la manera requerida para esta solución de tarea. El código para combinar colecciones separadas creadas se vuelve demasiado complicado, tal vez esté bien para fines de aprendizaje, pero no para aplicaciones reales. Una de las formas de resolver este problema es en el siguiente código:

class Program
{
    static void Main(string[] args)
    {
        List<FirstName> firstNames = new List<FirstName>();
        firstNames.Add(new FirstName { ID = 1, Name = "John" });
        firstNames.Add(new FirstName { ID = 2, Name = "Sue" });

        List<LastName> lastNames = new List<LastName>();
        lastNames.Add(new LastName { ID = 1, Name = "Doe" });
        lastNames.Add(new LastName { ID = 3, Name = "Smith" });

        HashSet<int> ids = new HashSet<int>();
        foreach (var name in firstNames)
        {
            ids.Add(name.ID);
        }
        foreach (var name in lastNames)
        {
            ids.Add(name.ID);
        }
        List<FullName> fullNames = new List<FullName>();
        foreach (int id in ids)
        {
            FullName fullName = new FullName();
            fullName.ID = id;
            FirstName firstName = firstNames.Find(f => f.ID == id);
            fullName.FirstName = firstName != null ? firstName.Name : string.Empty;
            LastName lastName = lastNames.Find(l => l.ID == id);
            fullName.LastName = lastName != null ? lastName.Name : string.Empty;
            fullNames.Add(fullName);
        }
    }
}
public class FirstName
{
    public int ID;

    public string Name;
}

public class LastName
{
    public int ID;

    public string Name;
}
class FullName
{
    public int ID;

    public string FirstName;

    public string LastName;
}

Si las colecciones reales son grandes para la formación de HashSet, en lugar de los bucles foreach se puede usar el siguiente código:

List<int> firstIds = firstNames.Select(f => f.ID).ToList();
List<int> LastIds = lastNames.Select(l => l.ID).ToList();
HashSet<int> ids = new HashSet<int>(firstIds.Union(LastIds));//Only unique IDs will be included in HashSet

0

¡Gracias a todos por las publicaciones interesantes!

Modifiqué el código porque en mi caso necesitaba

  • un predicado de unión personalizado
  • un comparador personalizado distintivo de unión

Para los interesados, este es mi código modificado (en VB, lo siento)

    Module MyExtensions
        <Extension()>
        Friend Function FullOuterJoin(Of TA, TB, TResult)(ByVal a As IEnumerable(Of TA), ByVal b As IEnumerable(Of TB), ByVal joinPredicate As Func(Of TA, TB, Boolean), ByVal projection As Func(Of TA, TB, TResult), ByVal comparer As IEqualityComparer(Of TResult)) As IEnumerable(Of TResult)
            Dim joinL =
                From xa In a
                From xb In b.Where(Function(x) joinPredicate(xa, x)).DefaultIfEmpty()
                Select projection(xa, xb)
            Dim joinR =
                From xb In b
                From xa In a.Where(Function(x) joinPredicate(x, xb)).DefaultIfEmpty()
                Select projection(xa, xb)
            Return joinL.Union(joinR, comparer)
        End Function
    End Module

    Dim fullOuterJoin = lefts.FullOuterJoin(
        rights,
        Function(left, right) left.Code = right.Code And (left.Amount [...] Or left.Description.Contains [...]),
        Function(left, right) New CompareResult(left, right),
        New MyEqualityComparer
    )

    Public Class MyEqualityComparer
        Implements IEqualityComparer(Of CompareResult)

        Private Function GetMsg(obj As CompareResult) As String
            Dim msg As String = ""
            msg &= obj.Code & "_"
            [...]
            Return msg
        End Function

        Public Overloads Function Equals(x As CompareResult, y As CompareResult) As Boolean Implements IEqualityComparer(Of CompareResult).Equals
            Return Me.GetMsg(x) = Me.GetMsg(y)
        End Function

        Public Overloads Function GetHashCode(obj As CompareResult) As Integer Implements IEqualityComparer(Of CompareResult).GetHashCode
            Return Me.GetMsg(obj).GetHashCode
        End Function
    End Class

0

Sin embargo, otra unión externa completa

Como no estaba tan contento con la simplicidad y la legibilidad de las otras proposiciones, terminé con esto:

No tiene la pretensión de ser rápido (aproximadamente 800 ms para unirse a 1000 * 1000 en una CPU 2020m: 2.4 ghz / 2 núcleos). Para mí, es solo una combinación externa completa compacta y casual.

Funciona igual que un SQL FULL OUTER JOIN (conservación duplicada)

Salud ;-)

using System;
using System.Collections.Generic;
using System.Linq;
namespace NS
{
public static class DataReunion
{
    public static List<Tuple<T1, T2>> FullJoin<T1, T2, TKey>(List<T1> List1, Func<T1, TKey> KeyFunc1, List<T2> List2, Func<T2, TKey> KeyFunc2)
    {
        List<Tuple<T1, T2>> result = new List<Tuple<T1, T2>>();

        Tuple<TKey, T1>[] identifiedList1 = List1.Select(_ => Tuple.Create(KeyFunc1(_), _)).OrderBy(_ => _.Item1).ToArray();
        Tuple<TKey, T2>[] identifiedList2 = List2.Select(_ => Tuple.Create(KeyFunc2(_), _)).OrderBy(_ => _.Item1).ToArray();

        identifiedList1.Where(_ => !identifiedList2.Select(__ => __.Item1).Contains(_.Item1)).ToList().ForEach(_ => {
            result.Add(Tuple.Create<T1, T2>(_.Item2, default(T2)));
        });

        result.AddRange(
            identifiedList1.Join(identifiedList2, left => left.Item1, right => right.Item1, (left, right) => Tuple.Create<T1, T2>(left.Item2, right.Item2)).ToList()
        );

        identifiedList2.Where(_ => !identifiedList1.Select(__ => __.Item1).Contains(_.Item1)).ToList().ForEach(_ => {
            result.Add(Tuple.Create<T1, T2>(default(T1), _.Item2));
        });

        return result;
    }
}
}

La idea es

  1. Construir ID basados ​​en constructores de funciones clave provistos
  2. Procesar solo elementos dejados
  3. Proceso de unión interna
  4. Procesar solo elementos correctos

Aquí hay una prueba sucinta que la acompaña:

Coloque un punto de interrupción al final para verificar manualmente que se comporta como se esperaba

using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NS;

namespace Tests
{
[TestClass]
public class DataReunionTest
{
    [TestMethod]
    public void Test()
    {
        List<Tuple<Int32, Int32, String>> A = new List<Tuple<Int32, Int32, String>>();
        List<Tuple<Int32, Int32, String>> B = new List<Tuple<Int32, Int32, String>>();

        Random rnd = new Random();

        /* Comment the testing block you do not want to run
        /* Solution to test a wide range of keys*/

        for (int i = 0; i < 500; i += 1)
        {
            A.Add(Tuple.Create(rnd.Next(1, 101), rnd.Next(1, 101), "A"));
            B.Add(Tuple.Create(rnd.Next(1, 101), rnd.Next(1, 101), "B"));
        }

        /* Solution for essential testing*/

        A.Add(Tuple.Create(1, 2, "B11"));
        A.Add(Tuple.Create(1, 2, "B12"));
        A.Add(Tuple.Create(1, 3, "C11"));
        A.Add(Tuple.Create(1, 3, "C12"));
        A.Add(Tuple.Create(1, 3, "C13"));
        A.Add(Tuple.Create(1, 4, "D1"));

        B.Add(Tuple.Create(1, 1, "A21"));
        B.Add(Tuple.Create(1, 1, "A22"));
        B.Add(Tuple.Create(1, 1, "A23"));
        B.Add(Tuple.Create(1, 2, "B21"));
        B.Add(Tuple.Create(1, 2, "B22"));
        B.Add(Tuple.Create(1, 2, "B23"));
        B.Add(Tuple.Create(1, 3, "C2"));
        B.Add(Tuple.Create(1, 5, "E2"));

        Func<Tuple<Int32, Int32, String>, Tuple<Int32, Int32>> key = (_) => Tuple.Create(_.Item1, _.Item2);

        var watch = System.Diagnostics.Stopwatch.StartNew();
        var res = DataReunion.FullJoin(A, key, B, key);
        watch.Stop();
        var elapsedMs = watch.ElapsedMilliseconds;
        String aser = JToken.FromObject(res).ToString(Formatting.Indented);
        Console.Write(elapsedMs);
    }
}

}


-4

Realmente odio estas expresiones linq, por eso existe SQL:

select isnull(fn.id, ln.id) as id, fn.firstname, ln.lastname
   from firstnames fn
   full join lastnames ln on ln.id=fn.id

Cree esto como vista sql en la base de datos e impórtelo como entidad.

Por supuesto, la unión (distinta) de las uniones izquierda y derecha también lo hará, pero es estúpido.


11
¿Por qué no dejar caer tantas abstracciones como sea posible y hacer esto en código máquina? (Sugerencia: porque las abstracciones de orden superior hacen la vida más fácil para el programador). Esto no responde la pregunta y me parece más una queja contra LINQ.
gastador

8
¿Quién dijo que los datos provienen de una base de datos?
user247702

1
Por supuesto, es una base de datos, hay palabras "combinación externa" en cuestión :) google.cz/search?q=outer+join
Milan Švec

1
Entiendo que esta es una solución "antigua", pero antes de votar, compare su complejidad con otras soluciones :) Excepto la aceptada, por supuesto, es la correcta.
Milan Švec

Por supuesto, puede ser una base de datos o no. Estoy buscando una solución con una unión externa entre listas en la memoria
edc65
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.