LINQ - 全外连接

248
我有一个人员ID和他们的名字列表,还有一个人员ID和他们的姓氏列表。有些人没有名字,有些人没有姓氏;我想在这两个列表上进行全外连接。
所以以下是列表:
ID  FirstName
--  ---------
 1  John
 2  Sue

ID  LastName
--  --------
 1  Doe
 3  Smith
应该产生:
ID  FirstName  LastName
--  ---------  --------
 1  John       Doe
 2  Sue
 3             Smith

我在stackoverflow上找到了很多关于“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
尝试使用.GroupJoin() https://dev59.com/fWUp5IYBdhLWcg3wAjxF - jdev.ninja
18个回答

237

更新1:提供一个真正通用的扩展方法FullOuterJoin

更新2:可选地接受键类型的自定义IEqualityComparer

更新3:此实现已经成为MoreLinq的一部分 - 感谢大家!(链接)

编辑:添加了FullOuterGroupJoin(ideone)。我重复使用了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 }

术语解释:

Joining(连接)是从关系型数据库设计中借用的术语:

  • Join(连接)将从ab两个表中有相应键(即“内部关联”)的记录组合成新的结果集,其中a中的元素将被重复和展开与b中的匹配元素数目一样多的次数,如果b为空,则不会返回任何结果。
  • Outer join(外关联)包括a中那些在b中没有对应元素的记录(即“左关联”),如果b为空,也会返回结果。
  • Full outer join(全关联)在结果集中包括ab中所有没有对应元素的记录,即使ab为空也会返回结果。

RDBMS 中不常见的是group join[1]

  • Group join(分组连接)与上述描述相同,但是它将具有相应键的记录进行分组,而不是在结果集中重复a中的元素。当您希望基于共同键枚举“连接”记录时,通常更方便使用此方法。

另请参见 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;
    }
}

5
不使用字典,您可以使用一个“Lookup”(查找),其中包含您的辅助扩展方法所表示的功能。例如,您可以将a.GroupBy(selectKeyA).ToDictionary()写成a.ToLookup(selectKeyA),将adict.OuterGet(key)写成alookup[key]。不过获取键的集合略微棘手:alookup.Select(x => x.Keys) - Risky Martin
1
@RiskyMartin 谢谢!这确实使整个事情更加优雅。我更新了答案和 ideone-s。(我想性能应该会提高,因为实例化的对象更少)。 - sehe
FullOuterJoin如何修改以接受(并使用)IEqualityComparer<>? - Jimmy
谢谢!那么ToLookup()不需要使用EqualityComparer吗? - Jimmy
1
@Revious,这仅适用于您知道键是唯一的情况。而对于/grouping/来说,这并不是常见情况。除此之外,是的,请尽管使用。如果您知道哈希不会拖慢性能(基于节点的容器原则上具有更高的成本,并且哈希不是免费的,效率取决于哈希函数/桶分布),那么它肯定会更具算法效率。因此,对于小负载,我认为它可能不会更快。 - sehe
显示剩余13条评论

140

我不知道这是否涵盖了所有情况,逻辑上看起来是正确的。思路是取左外连接和右外连接,然后取结果的并集。

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

5
联合操作将消除重复项。如果您不希望出现重复项,或者可以编写第二个查询来排除第一个查询中包含的任何内容,请使用Concat。这是SQL中UNION和UNION ALL之间的区别。 - cadrell0
5
如果一个人有名字和姓氏,会出现重复的情况,因此联合是一个有效的选择。 - saus
1
@saus 但是有一个ID列,所以即使有重复的名字和姓氏,ID也应该是不同的。 - cadrell0
1
你的解决方案适用于基本类型,但似乎不适用于对象。在我的情况下,FirstName是一个域对象,而LastName是另一个域对象。当我联合这两个结果时,LINQ抛出了一个NotSupportedException(Union或Concat中的类型构造不兼容)。你有遇到过类似的问题吗? - Candy Chiu
1
@CandyChiu:我实际上从未遇到过这种情况。我猜这是您查询提供程序的限制。在执行联合/连接之前,您可能需要使用LINQ to Objects通过调用AsEnumerable()。尝试一下,看看效果如何。如果这不是您想要走的路线,那么我无法提供更多帮助了。 - Jeff Mercado
显示剩余4条评论

49

我认为这些大多数都有问题,包括被接受的答案,因为它们在使用IQueryable时不能很好地工作,要么由于做了太多的服务器往返和过多的数据返回,要么是执行了太多的客户端操作。

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

此外,大多数其他方法实际上并没有实现正确的全外连接,因为它们使用带右连接的Union而不是带右反半连接的Concat,这不仅从结果中消除了重复的内部连接行,而且还消除了左侧或右侧数据中原来存在的任何真正的重复项。

所以这里是我的扩展,解决了所有这些问题,能够生成SQL,并直接在LINQ to SQL中实现连接,在服务器上执行,比Enumerable中的其他方法更快,使用的内存更少:

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 或者源代码中,右反半连接(Right Anti-Semi-Join)的区别不大,但在服务器(SQL)端会对最终答案产生影响,消除了不必要的 JOIN。手动编写 Expression 来处理将 Expression<Func<>> 合并到 lambda 中的过程可以通过 LinqKit 进行改进,但如果语言/编译器能为此提供一些帮助就更好了。为了完整性考虑,包括了 FullOuterJoinDistinctRightOuterJoin 函数,但我还没有重新实现 FullOuterGroupJoin。我为可排序键的情况编写了 另一个版本IEnumerable 全外连接,它比将左外连接与右反半连接相结合要快约50%,至少在小集合上是这样。它只需要在排序后遍历每个集合一次即可。我还添加了另一个答案,用于适用于 EF 的版本,通过替换 Invoke 来进行自定义扩展。

是的,它们只是用来捕获 TPTCTResult 类型以创建正确的 Expression<Func<>> 的。我想我可以用 ______ 来替换它们,但在 C# 有一个适当的参数通配符可用之前,这似乎并不更清晰。 - NetMage
1
@MarcL。我不太确定“烦人”这个词是否合适,但我同意这个答案在这种情况下非常有用。令人印象深刻的东西(尽管对我来说,它证实了Linq-to-SQL的缺点)。 - sehe
3
我收到了“LINQ表达式节点类型'Invoke'在LINQ to Entities中不受支持。”的错误信息。这段代码有任何限制吗?我想在IQueryables上执行FULL JOIN。 - Learner
1
我已经添加了一个新的答案,用自定义的ExpressionVisitor替换了Invoke以内联Invoke,这样它应该可以与EF一起使用。你能试试吗? - NetMage
1
使用LeftOuterJoin和RightOuterJoin方法来实现FullOuterJoin是一种优雅的方法,但会导致多次枚举。对于仅支持向前遍历的流来说,这将失败。 - John Zabroski
显示剩余12条评论

10

我猜@sehe的方法更加强大,但在我更好地理解它之前,我会参考@MichaelSander的扩展进行跳跃。我修改了它以匹配内置的Enumerable.Join()方法的语法和返回类型,该方法在此处描述。我在尊重@cadrell0对@JeffMercado解决方案下的评论中添加了“distinct”后缀。

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

9

这里有一个扩展方法可以实现这个功能:

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
除了 Union 会删除重复项,所以如果原始数据中有重复行,则它们不会出现在结果中。 - NetMage
太好了!如果你需要防止重复被移除,可以添加一个唯一的ID。是的,使用UNION操作可能有点浪费,除非你能表明存在唯一的ID,并通过内部启发式/优化将UNION操作转换为UNION ALL操作。但它可以工作。 - TamusJRoyce
被接受的答案相同。 - Gert Arnold
我该如何使用“Where”子句对结果进行筛选? - Steven Sann

5
正如你所发现的,Linq没有"outer join"关键字构造,因此需要使用你提到的查询来实现左外连接。为此,你可以添加lastname列表中未在连接集合中表示的任何元素,从而产生与全连接相同的结果集。
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)));
                  

你错了。LINQ有外连接,Enumerable.DefaultIfEmpty()可以生成它。但是LINQ没有完全外连接。 - Tanveer Badar

5

我提供的解决方案适用于在两个可枚举对象中键名唯一的情况:

 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

4

我喜欢sehe的回答,但它没有使用延迟执行(通过调用ToLookup来急切地枚举输入序列)。因此,在查看.NET源代码LINQ-to-objects之后,我想到了这个:

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将枚举左侧的某个顺序,然后是右侧的某个不在左侧的顺序。但元素的关系顺序不被保留。 - Ivan Danilov
@IvanDanilov,你说得对,这实际上并没有在合同中。然而,ToLookup的实现使用了Enumerable.cs中的内部Lookup类,它将分组保留在插入有序的链表中,并使用此列表来迭代它们。因此,在当前的.NET版本中,顺序是有保证的,但由于微软不幸地没有记录这一点,他们可能会在以后的版本中进行更改。 - Søren Boisen
我在Win 8.1上尝试了.NET 4.5.1,但它不能保持顺序。 - Ivan Danilov
1
“..输入序列通过对ToLookup的调用被急切地枚举。但是你的实现完全一样...使用Yielding在这里并没有太大作用,因为有限状态机的开销比较大。” - pkuderov
4
当请求结果的第一个元素时,才会执行查找调用,而不是在创建迭代器时执行。这就是所谓的延迟执行。你可以进一步延迟枚举一个输入集,通过直接迭代左侧 Enumerable 而不是将其转换为 Lookup,这样可以获得额外的好处,即保留左侧集合的顺序。 - Rolf
显示剩余2条评论

3

我决定将这作为一个单独的答案加入,因为我不确定它是否已经经过充分测试。这是使用基本上是简化的、定制化的 LINQKit Invoke/Expand 对于 Expression 的重新实现 FullOuterJoin 方法,以便它可以在 Entity Framework 中工作。由于与我的之前的答案几乎相同,所以没有太多解释。

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: Argument Types do not match]。这是真的,因为我正在使用[Guid] TKey,而在某些时候,空访问者期望一个[Guid?]类型。也许我漏掉了什么。我已经为EF 6.4.4编写了一个简短的示例。请告诉我如何与您分享此代码。谢谢! - Troncho
我也在使用LinqPad进行测试。我的查询抛出了ArgumentException异常,所以我决定在[.Net Framework 4.7.1]和最新的EF 6上使用VS2019进行调试。在那里,我找到了真正的问题。为了测试你的代码,我生成了两个来自同一[Persons]表的不同数据集。我过滤了这两个数据集,使得某些记录只存在于每个数据集中,而有些记录则同时存在于两个数据集中。[PersonId]是一个[Primary Key] Guid (c#) / Uniqueidentifier (SqlServer),两个数据集都不会生成任何空的[PersonId]值。共享代码:https://github.com/Troncho/EF_FullOuterJoin - Troncho
当我将其与另一个类一起使用时,使用select时出现了此错误:“无法创建类型为'TestProject.Contollers.TableViewModel'的空常量值。在此上下文中,仅支持实体类型、枚举类型或基元类型。” - Hosam.Yousof
以下是代码: var first = context.tt1.Select(e => new TableViewModel { ID = e.ID1, Title = e.Title1, }); var second = context.tt2.Select(e => new TableViewModel { ID = e.ID1, Title = e.Title, }); var list = first.FullOuterJoin(second, a => a.ID, b => b.ID, (a, b) => new { a, b }).ToList(); } - Hosam.Yousof
@Hosam.Yousof 如果您尝试使用“Join”替换“FullOuterJoin”,会发生什么? - NetMage
显示剩余2条评论

1
我已经写了这个扩展类大约6年了,与许多解决方案一起使用并且没有问题。希望它能有所帮助。
编辑:我注意到有些人可能不知道如何使用扩展类。
要使用这个扩展类,只需在您的类中通过添加以下行来引用其命名空间: using 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 的表达式树。 - O. R. Mapper
edc65。我知道如果你已经做过了,这可能是个愚蠢的问题。但以防万一(正如我注意到的有些人不知道),你只需要引用命名空间joinext。 - H7O
O. R. Mapper,请告诉我您希望它使用哪种类型的集合。 它应该可以与任何IEnumerable集合一起正常工作。 - H7O

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接