LINQ-完全外部联接


202

我有一个人的ID和他们的姓氏列表,以及一个人的ID和他们的姓氏列表。有些人没有名字,有些人没有姓;我想在两个列表上进行完全外部联接。

因此,以下列表:

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

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

应产生:

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

我是LINQ的新手(如果我很la脚,请原谅我),并且已经找到了许多解决方案,这些解决方案看起来都非常相似,但实际上似乎是外部联接,而对于“ LINQ外部联接”。

到目前为止,我的尝试是这样的:

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

但这返回:

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

我究竟做错了什么?


2
您是否需要此功能仅适用于内存中列表或Linq2Sql?
JamesFaix

Answers:


122

我不知道这是否涵盖所有情况,从逻辑上讲似乎是正确的。这个想法是采用左外部联接和右外部联接,然后取结果的并集。

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

由于它是在LINQ to Objects中编写的,因此按其编写的方式工作。如果是LINQ to SQL或其他,则查询处理器可能不支持安全导航或其他操作。您必须使用条件运算符有条件地获取值。

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
联合会消除重复。如果您不希望重复,或者可以编写第二个查询以排除第一个查询中包含的任何内容,请改用Concat。这是UNION和UNION ALL之间的SQL区别
cadrell0 2011年

3
如果一个人有一个名字和一个姓氏,@ cadre110将出现重复,因此合并是一个有效的选择。
2011年

1
@saus但有一个ID列,因此即使名字和姓氏重复,ID也应不同
cadrell0 2011年

1
您的解决方案适用于原始类型,但似乎不适用于对象。就我而言,FirstName是一个域对象,而LastName是另一个域对象。当我合并两个结果时,LINQ抛出了NotSupportedException(Union或Concat中的类型构造不兼容)。您是否遇到过类似的问题?
赵超颖2012年

1
@CandyChiu:我实际上从未遇到过这种情况。我想这是您的查询提供程序的局限性。在这种情况下,您可能希望通过AsEnumerable()在执行联合/串联之前调用LINQ to Objects 。尝试一下,看看效果如何。如果这不是您要走的路线,那么我不确定我是否能提供更多帮助。
Jeff Mercado 2012年

196

更新1:提供真正的通用扩展方法FullOuterJoin
更新2 :(可选)接受IEqualityComparer键类型的自定义
更新3:此实现最近MoreLinq成为一部分 -谢谢!

编辑已添加FullOuterGroupJoinideone)。我重用了该GetOuter<>实现,使它的性能降低了一点,但是现在我的目标是“高级”代码,而不是最先进的代码。

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

打印输出:

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

您还可以提供默认值: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)" }
        )

印刷:

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

所用术语的解释:

连接是从关系数据库设计中借用的一个术语:

  • 一个加盟将重复的元素从a多次出现在元素b 与相应的按键(即:什么,如果b是空的)。数据库术语称此inner (equi)join
  • 一个外连接包括从元件a用于其中没有相应的元件中存在b。(即:即使结果b为空也是如此)。这通常称为left join
  • 完全外部连接包括从记录a 以及b如果没有相应的元件中的其他存在。(即结果a为空)

东西不通常在RDBMS看到的是一组加入[1]

  • 组加入,进行如上所述,所描述的相同,但代替重复从元件a对应于多个b,它基团与相应的键的记录。当您希望基于公共密钥通过“联接”记录进行枚举时,这通常更方便。

另请参阅GroupJoin,其中也包含一些常规背景说明。


[1](我相信Oracle和MSSQL具有对此的专有扩展)

完整代码

为此的通用“插入式”扩展类

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

编辑以显示所FullOuterJoin提供扩展方法的用法
sehe 2012年

编辑:添加了FullOuterGroupJoin扩展方法
2012年

4
可以使用Lookup来代替使用Dictionary,而使用Lookup包含辅助扩展方法中表达的功能。例如,您可以编写a.GroupBy(selectKeyA).ToDictionary();as a.ToLookup(selectKeyA)adict.OuterGet(key)as alookup[key]。不过,获取键的集合比较麻烦alookup.Select(x => x.Keys)
风险的马丁

1
@RiskyMartin谢谢!确实,这使整个事情变得更加优雅。我更新了答案 ideone-s。(我想应该提高性能,因为实例化了更少的对象)。
sehe 2012年

1
@Revious仅在您知道键是唯一的时才起作用。这不是/ grouping /的常见情况。除此之外,是的,一定。如果您知道散列不会拖累性能(原则上基于节点的容器具有更高的成本,并且散列不是免费的,并且效率取决于散列函数/存储桶的扩展),那么它肯定会在算法上更加有效。因此,对于较小的负载,我希望它可能不会更快
sehe

27

我认为其中大多数都存在问题,包括已接受的答案,因为由于服务器往返次数过多,数据返回过多或客户端执行过多,因此它们无法与Linq一起通过IQueryable很好地工作。

对于IEnumerable,我不喜欢Sehe的答案或类似的答案,因为它使用了过多的内存(一个简单的10000000两个列表测试在我的32GB机器上耗尽了Linqpad的内存)。

另外,大多数其他人实际上并没有实现适当的完全外部联接,因为他们使用的是带有右联接的联合,而不是带有右反半联接的Concat,这不仅从结果中消除了重复的内部联接行,而且左侧或右侧数据中最初存在的任何适当的重复项。

因此,以下是我的扩展程序,它们处理所有这些问题,生成SQL并在LINQ中直接实现到SQL的联接,在服务器上执行,并且比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));
    }
}

正确的反半联接之间的差异主要是通过Linq to Objects或源代码来解决,但在最终回答中在服务器(SQL)方面有所不同,从而消除了不必要的问题 JOIN

的手编码 ExpressionExpression<Func<>>使用LinqKit可以改进用于将a 合并为lambda,但是如果语言/编译器为此添加了一些帮助,那就很好了。该FullOuterJoinDistinctRightOuterJoin功能出于完整性考虑,但我没有重新实现FullOuterGroupJoin呢。

我写了另一个版本的完全外部联接IEnumerable键是可排序的情况,至少在较小的集合上,它比左外部联接与右反半联接相结合的速度快约50%。它只排序一次,然后遍历每个集合。

我还为与EF兼容的版本添加了另一个答案,方法是Invoke使用自定义扩展替换。


怎么TP unusedP, TC unusedC办?他们真的没有使用吗?
Rudey

是的,他们只是存在于捕捉的类型TPTCTResult创建正确的Expression<Func<>>。我认为我可以取代他们______相反,但似乎没有任何清晰的,直到C#有一个适当的参数通配符代替使用。
NetMage

1
@马克 我不太确定“累人”-但我同意这个答案在这种情况下非常有用。令人印象深刻的东西(尽管对我来说,这证明了Linq-to-SQL的缺点)
sehe

3
我越来越The LINQ expression node type 'Invoke' is not supported in LINQ to Entities.。该代码有任何限制吗?我想通过IQueryables进行完全加入
学习者

1
我添加了一个新答案,将其替换为内联Invoke自定义的自定义ExpressionVisitorInvoke因此它应可与EF一起使用。你可以试试看吗?
NetMage

7

这是执行此操作的扩展方法:

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),这意味着完全外部联接=左外部联接联合所有右外部联接!我赞赏这种方法的简单性。
TamusJRoyce

1
@TamusJRoyce Except Union删除重复项,因此,如果原始数据中存在重复行,则结果中将不会包含重复行。
NetMage

好点!如果需要防止删除重复项,请添加唯一的ID。是。除非您可以暗示存在唯一的ID,并且该联合切换为全部联合(通过内部启发式/优化),否则联合会有点浪费。但这会起作用。
TamusJRoyce,


7

我猜@sehe的方法更强,但是直到我更好地理解它为止,我发现自己已经从@MichaelSander的扩展名中跳了下来。我对其进行了修改,以匹配此处描述的内置Enumerable.Join()方法的语法和返回类型。我在@JeffMercado解决方案下针对@ cadrell0的注释添加了“与众不同”的后缀。

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

}

在示例中,您将像这样使用它:

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

将来,随着我学到更多,我觉得我会逐渐流行@sehe的逻辑。但是即使那样,我仍然要小心,因为如果可行,我认为至少要有一个与现有“ .Join()”方法的语法相匹配的重载很重要,原因有两个:

  1. 方法的一致性有助于节省时间,避免错误以及避免意外行为。
  2. 如果将来有一个现成的“ .FullJoin()”方法,我想它会尽量保持当前存在的“ .Join()”方法的语法。如果是这样,那么如果要迁移到它,则可以简单地重命名函数,而无需更改参数或担心不同的返回类型会破坏代码。

我仍然对泛型,扩展,Func语句和其他功能不熟悉,因此欢迎反馈。

编辑:没多久我就意识到我的代码有问题。我在LINQPad中执行.Dump()并查看返回类型。它只是IEnumerable,所以我尝试匹配它。但是,当我在扩展名上实际执行.Where()或.Select()时,出现错误:“'System Collections.IEnumerable'不包含'Select'和...的定义”。因此,最终我可以匹配.Join()的输入语法,但不能匹配返回行为。

编辑:添加“ TResult”到函数的返回类型。在阅读Microsoft文章时错过了这一点,这当然是有道理的。通过此修复程序,现在看来返回行为毕竟符合我的目标。


+2以及迈克尔·桑德斯(Michael Sanders)。我不小心单击了此按钮,投票被锁定。请添加两个。
TamusJRoyce

@TamusJRoyce,我刚刚编辑了一些代码格式。我相信进行编辑后,您可以选择重新投票。如果愿意,可以试一试。
pwilcox

非常感谢!
Roshna Omer's

6

如您所见,Linq没有“外部联接”构造。您获得的最接近的结果是使用您所述查询的左外部联接。为此,您可以添加姓氏列表中未在联接中表示的任何元素:

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

我喜欢sehe的答案,但是它不使用延迟执行(对ToLookup的调用急切地列举了输入序列)。因此,在查看了.NET的LINQ到对象的源代码之后,我想到了:

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

此实现具有以下重要属性:

  • 延迟执行时,在枚举输出序列之前不会枚举输入序列。
  • 每个输入序列仅枚举一次。
  • 保留输入序列的顺序,从某种意义上说,它将按左序列然后右的顺序生成元组(对于在左序列中不存在的键)。

这些属性很重要,因为它们是FullOuterJoin的新手,但是对LINQ有经验。


它不保留输入序列的顺序:查找不能保证一定的顺序,因此这些foreach将以左侧的某个顺序枚举,然后右侧的某个顺序不出现在左侧。但是不能保留元素的关系顺序。
伊万·丹尼洛夫

@IvanDanilov您是正确的,这实际上不在合同中。但是,ToLookup的实现在Enumerable.cs中使用内部Lookup类,该类将分组保留在按插入顺序排列的链表中,并使用此列表进行迭代。因此,在当前的.NET版本中,顺序是可以保证的,但是遗憾的是,由于MS尚未对此进行记录,因此他们可以在以后的版本中进行更改。
索伦Boisen

我在Win 8.1的.NET 4.5.1上尝试过它,但它不保留顺序。
伊万·丹尼洛夫

1
“ ..通过调用ToLookup急切地枚举了输入序列”。但是您的实现却完全相同。由于在有限状态机上的花费,在这里屈服并不多。
pkuderov

4
当请求结果的第一个元素时(而不是在创建迭代器时),将完成Lookup调用。这就是延迟执行的意思。您可以通过直接迭代左Enumerable而不是将其转换为Lookup来进一步推迟一个输入集的枚举,从而保留了左集的顺序,从而带来了额外的好处。
罗尔夫

2

我决定将此添加为单独的答案,因为我不确定它是否经过充分测试。这是对FullOuterJoin使用实质上的简化,定制版的方法LINQKit Invoke/ ExpandExpression,这样它应该工作的实体框架。没有太多的解释,因为它与我之前的答案几乎相同。

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,令人印象深刻的编码!当我用一个简单的示例运行它时,并且在[base.Visit(Node)]中调用了[NullVisitor.Visit(..)时,它会抛出[System.ArgumentException:参数类型不匹配]。的确如此,因为我正在使用[Guid] TKey,并且在某些时候空访问者期望使用[Guid?]类型。可能是我缺少了一些东西。我有一个为EF 6.4.4编码的简短示例。请让我知道如何与您共享此代码。谢谢!
特隆乔

@Troncho我通常使用LINQPad进行测试,因此EF 6并非易事。base.Visit(node)不应该抛出异常,因为这只会沿着树递归。我几乎可以访问任何代码共享服务,但不能设置测试数据库。但是,针对我的LINQ to SQL测试运行它似乎很好。
NetMage

@Troncho您是否有可能在某个Guid键和一个Guid?外键之间进行联接?
NetMage

我也使用LinqPad进行测试。我的查询引发了ArgumentException,因此我决定在[.Net Framework 4.7.1]和最新的EF 6上的VS2019上对其进行调试。在那里,我找到了真正的问题。为了测试您的代码,我将从同一个[Persons]表中生成2个单独的数据集。我对两个集合都进行了过滤,以便某些记录对于每个集合都是唯一的,并且在两个集合上都存在一些记录。[PersonId]是[主键] Guid(c#)/ Uniqueidentifier(SqlServer),并且两个集合都不生成任何空的[PersonId]值。共享代码:github.com/Troncho/EF_FullOuterJoin
Troncho

1

在两个输入上执行内存中的流枚举,并为每一行调用选择器。如果当前迭代没有相关性,则选择器参数之一将为null

例:

   var result = left.FullOuterJoin(
         right, 
         x=>left.Key, 
         x=>right.Key, 
         (l,r) => new { LeftKey = l?.Key, RightKey=r?.Key });
  • 需要IComparer作为关联类型,如果没有提供,则使用Comparer.Default。

  • 要求将“ OrderBy”应用于输入枚举

    /// <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
这是使事情“流式传输”的英勇努力。可悲的是,所有的收益都在第一步失去了,您OrderBy在这两个关键的投影上都执行了。OrderBy出于显而易见的原因,缓冲整个序列
sehe

@sehe对于Linq to Objects,您绝对正确。如果IEnumerable <T>是IQueryable <T>,则源应该排序-尽管没有时间进行测试。如果对此我错了,只需将输入IEnumerable <T>替换为IQueryable <T>,就应该在源/数据库中进行排序。
詹姆斯·卡拉多克·戴维斯

1

对于这两种枚举键都是唯一的情况,我的干净解决方案是:

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

所以

    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();

输出:

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

0

两个或多个表的完全外部联接:首先提取要联接的列。

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

然后在提取的列和主表之间使用左外部联接。

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

我大概在6年前就为应用程序编写了这个扩展类,从那时起,在很多没有问题的解决方案中就一直使用它。希望能帮助到你。

编辑:我注意到有些人可能不知道如何使用扩展类。

要使用此扩展类,只需使用joinext添加以下行即可在您的类中引用其名称空间;

^这应该使您能够看到碰巧使用的任何IEnumerable对象集合上的扩展功能的智能感知。

希望这可以帮助。让我知道它是否仍然不清楚,希望我会写一个示例示例说明如何使用它。

现在是班级:

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
不幸的是,似乎其中的函数似乎SelectMany无法转换为值得LINQ2SQL使用的表达式树。
OR Mapper

edc65。我知道您是否已经做过这可能是一个愚蠢的问题。但是以防万一(正如我注意到的一些未知),您只需要引用命名空间joinext。
H7O

或Mapper,请告知您您希望它使用哪种类型的馆藏。它可以与任何IEnumerable集合一起正常工作
-H7O,

0

我认为LINQ join子句不是此问题的正确解决方案,因为join子句的目的不是按照此任务解决方案所需的方式来累积数据。合并创建的单独集合的代码变得太复杂了,也许出于学习目的是可以的,但对于实际应用程序却不是。下面的代码是解决此问题的方法之一:

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

如果真正的集合对于HashSet的形成来说很大,那么可以使用foreach循环使用以下代码:

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

谢谢大家的有趣帖子!

我修改了代码,因为我需要

  • 一个个性化的连接谓词
  • 一个个性化的工会不同的比较器

对于那些感兴趣的人,这是我修改的代码(在VB中,抱歉)

    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

另一个完全外部联接

由于对其他命题的简单性和可读性不满意,我最终得出以下结论:

它没有快速的预兆(在2020m CPU上加入1000 * 1000大约需要800毫秒:2.4ghz / 2cores)。对我而言,这只是紧凑而随意的完全外部连接。

它的工作方式与SQL FULL OUTER JOIN相同(重复保存)

干杯;-)

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

这个想法是为了

  1. 根据提供的关键功能构建器构建ID
  2. 处理仅剩下的项目
  3. 处理内部联接
  4. 仅处理权利项目

这是一个简短的测试:

在末端放置一个断点以手动验证其行为是否符合预期

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

我真的很讨厌这些linq表达式,这就是为什么存在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

在数据库中将其创建为sql视图,并将其作为实体导入。

当然,左右连接的(明显)联合也可以做到这一点,但这很愚蠢。


11
为什么不仅仅丢弃尽可能多的抽象,并在机器代码中这样做呢?(提示:因为高阶抽象使程序员的生活更轻松)。这没有回答问题,在我看来更像是对LINQ的咆哮。
支出者2016年

8
谁说数据来自数据库?
user247702

1
当然,它是数据库,还有“ outer join”这个词有问题:) google.cz/search?q=outer+join
MilanŠvec18-10-19

1
我知道这是“过时的”解决方案,但是在拒绝投票之前,请将其复杂性与其他解决方案进行比较:)除了被接受的解决方案以外,它当然是正确的。
米兰Švec'18 -10-19

当然也可以是数据库。我正在寻找一个在内存中的列表之间具有外部
联接
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.