【问题标题】:LINQ - Full Outer JoinLINQ - 完全外部联接
【发布时间】:2011-07-26 06:55:42
【问题描述】:

我有一个人的 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 的新手(如果我跛脚,请原谅我)并且已经找到了很多“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

我做错了什么?

【问题讨论】:

标签: c# .net linq outer-join full-outer-join


【解决方案1】:

如您所见,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)));

【讨论】:

  • 你错了。 LINQ 有外连接, Enumerable.DefautIfEmpty() 确实会生成它。 LINQ 没有的是全外连接。
【解决方案2】:

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

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

【讨论】:

  • 联合将消除重复。如果您不期望重复,或者可以编写第二个查询来排除第一个中包含的任何内容,请改用 Concat。这是 UNION 和 UNION ALL 的 SQL 区别
  • @cadre110 如果一个人有名字和姓氏,则会出现重复,因此 union 是一个有效的选择。
  • @saus 但是有一个ID列,所以即使名字和姓氏重复,ID也应该不同
  • 您的解决方案适用于原始类型,但似乎不适用于对象。在我的例子中,FirstName 是一个域对象,而 LastName 是另一个域对象。当我合并这两个结果时,LINQ 抛出了 NotSupportedException(Union 或 Concat 中的类型构造不兼容)。您是否遇到过类似的问题?
  • @CandyChiu:其实我从来没有遇到过这种情况。我想这是您的查询提供程序的限制。在这种情况下,您可能希望通过在执行联合/连接之前调用AsEnumerable() 来使用 LINQ to Objects。试试看,看看效果如何。如果这不是您想要的路线,我不确定我能提供更多帮助。
【解决方案3】:

更新 1:提供真正通用的扩展方法 FullOuterJoin
更新 2:可选择接受自定义的 IEqualityComparer 作为密钥类型
更新 3:这个实现有 recently become part of MoreLinq - 谢谢大家!

编辑添加了FullOuterGroupJoin (ideone)。我重用了GetOuter&lt;&gt; 实现,这使它的性能比它可能的要低一点,但我现在的目标是“高级”代码,而不是前沿优化。

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 }

所用术语的解释:

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

  • join 将重复 a 中的元素与 b 中的元素一样多具有相应的键(即:如果 b 为空,则没有任何内容)。 数据库术语称之为inner (equi)join
  • 外部联接包括来自a 的元素,没有对应的 元素 存在于b 中。 (即:即使b 为空,也会产生结果)。 这通常被称为left join
  • 完全外部联接包括来自a以及b的记录,如果没有对应的元素存在于另一个中。 (即,即使a 为空,也会产生结果)

在 RDBMS 中通常不常见的是组加入[1]

  • 组加入,作用与上述相同,不是为多个对应的b 重复来自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;
    }
}

【讨论】:

  • 已编辑以显示提供的FullOuterJoin 扩展方法的用法
  • 已编辑:添加了 FullOuterGroupJoin 扩展方法
  • 您可以使用Lookup,而不是使用字典,它包含在您的辅助扩展方法中表达的功能。例如,您可以将a.GroupBy(selectKeyA).ToDictionary(); 写为a.ToLookup(selectKeyA),将adict.OuterGet(key) 写为alookup[key]。不过,获取密钥集合有点棘手:alookup.Select(x =&gt; x.Keys)
  • @RiskyMartin 谢谢!确实,这使整个事情变得更加优雅。我更新了答案 ideone-s。 (我认为应该提高性能,因为实例化的对象更少)。
  • @Revious 仅在您知道密钥是唯一的情况下才有效。这不是 /grouping/ 的常见情况。除此之外,是的,无论如何。如果你知道散列不会拖累性能(基于节点的容器原则上成本更高,散列不是免费的,效率取决于散列函数/桶的传播),它肯定会在算法上更有效。所以,对于小负载,我希望它可能不会更快
【解决方案4】:

这是一个扩展方法:

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

【讨论】:

  • +1。 R ⟗ S = (R ⟕ S) ∪ (R ⟖ S),意思是full outer join = left outer join union all right outer join!我很欣赏这种方法的简单性。
  • @TamusJRoyce 除了Union 会删除重复行,因此如果原始数据中有重复行,则不会出现在结果中。
  • 好点!如果您需要防止删除重复项,请添加唯一 ID。是的。联合有点浪费,除非你可以暗示有一个唯一的 id 并且联合切换到联合所有(通过内部启发式/优化)。但它会起作用。
  • 如何通过Where 子句过滤出结果?
【解决方案5】:

我猜@sehe 的方法更强大,但在我更好地理解它之前,我发现自己正在跳过@MichaelSander 的扩展。我对其进行了修改以匹配here 描述的内置 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”。在阅读微软文章时错过了这一点,当然这是有道理的。有了这个修复,现在看来返回行为毕竟符合我的目标。

【讨论】:

  • +2 这个答案以及迈克尔·桑德斯。我不小心点击了这个,投票被锁定了。请添加两个。
  • @TamusJRoyce,我刚进去编辑了一下代码格式。我相信在进行编辑后,您可以选择重新投票。如果你愿意,可以试一试。
  • 非常感谢!
【解决方案6】:

我真的很讨厌这些 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 视图并将其作为实体导入。

当然,左右连接的(不同的)联合也可以,但它很愚蠢。

【讨论】:

  • 为什么不尽可能多地放弃抽象并在机器代码中这样做呢? (提示:因为更高阶的抽象使程序员的生活更轻松)。这没有回答问题,在我看来更像是对 LINQ 的咆哮。
  • 谁说数据来自数据库?
  • 当然是数据库,有问题的“外连接” :) google.cz/search?q=outer+join
  • 我知道这是“老式”解决方案,但在否决之前,将其复杂性与其他解决方案进行比较 :) 除了接受的解决方案之外,它当然是正确的解决方案。
  • 当然可以是数据库也可以不是。我正在寻找在内存中的列表之间进行外部连接的解决方案
【解决方案7】:

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

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 的人所期望的。

【讨论】:

  • 它不保留输入序列的顺序:查找不保证这一点,因此这些 foreaches 将按左侧的某种顺序枚举,然后在左侧不存在某些右侧顺序。但不保留元素的关系顺序。
  • @IvanDanilov 你说得对,这实际上不在合同中。然而,ToLookup 的实现使用 Enumerable.cs 中的一个内部 Lookup 类,该类将分组保存在一个按插入排序的链接列表中,并使用该列表遍历它们。因此,在当前的 .NET 版本中,订单是有保证的,但不幸的是,由于 MS 没有记录这一点,他们可以在以后的版本中进行更改。
  • 我在 Win 8.1 上的 .NET 4.5.1 上尝试过,它不保留顺序。
  • "..输入序列被对 ToLookup 的调用急切地枚举"。但是您的实现完全一样。由于有限状态机的费用,这里的收益并不多。
  • 查找调用在请求结果的第一个元素时完成,而不是在创建迭代器时完成。这就是延期执行的意思。您可以通过直接迭代左 Enumerable 而不是将其转换为 Lookup 来进一步推迟一个输入集的枚举,从而获得保留左集顺序的额外好处。
【解决方案8】:

我可能在 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)
                        ));

        }

    }
}

【讨论】:

  • 不幸的是,SelectMany 中的函数似乎无法转换为适合 LINQ2SQL 的表达式树。
  • edc65.我知道如果你已经这样做了,这可能是一个愚蠢的问题。但以防万一(我注意到有些人不知道),您只需要引用命名空间 joinext。
  • O. R. Mapper,让我知道您希望它使用哪种类型的集合。它应该适用于任何 IEnumerable 集合
【解决方案9】:

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

例子:

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

【讨论】:

  • 这是一项使事物“流式传输”的英勇努力。可悲的是,所有收益都在第一步中丢失,您在两个关键预测上都执行OrderByOrderBy buffers the entire sequence, for the obvious reasons.
  • @sehe 对于 Linq to Objects,您绝对是正确的。如果 IEnumerable 是 IQueryable 源应该排序 - 虽然没有时间测试。如果我对此有误,只需将输入 IEnumerable 替换为 IQueryable 即可在源/数据库中排序。
【解决方案10】:

我认为其中大多数都存在问题,包括接受的答案,因为它们不能很好地与 Linq over IQueryable 一起使用,因为执行了太多的服务器往返和太多的数据返回,或者执行了太多的客户端执行.

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

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

所以这是我的扩展,它们处理所有这些问题,生成 SQL 以及直接在 LINQ to 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));
    }
}

Right Anti-Semi-Join 之间的区别主要在于 Linq to Objects 或源代码中没有实际意义,但在最终答案中的服务器 (SQL) 端有所不同,删除了不必要的 JOIN

使用 LinqKit 可以改进 Expression 的手动编码以处理将 Expression&lt;Func&lt;&gt;&gt; 合并到 lambda,但如果语言/编译器为此添加了一些帮助会很好。包含FullOuterJoinDistinctRightOuterJoin 函数是为了完整性,但我还没有重新实现FullOuterGroupJoin

我为IEnumerable 写了another version 的完整外连接,用于键可排序的情况,这比将左外连接与右反半连接组合快约50%,至少在小型集合上是这样。它仅在排序一次后遍历每个集合。

我还添加了 another answer 用于与 EF 一起使用的版本,方法是将 Invoke 替换为自定义扩展。

【讨论】:

  • TP unusedP, TC unusedC 是怎么回事?它们真的没有被使用吗?
  • 是的,它们只是用于捕获TPTCTResult 中的类型以创建正确的Expression&lt;Func&lt;&gt;&gt;。我想我可以用______ 代替它们,但是在 C# 使用适当的参数通配符之前,这似乎并不清晰。
  • @MarcL。我不太确定“令人厌烦” - 但我同意这个答案在这种情况下非常有用。令人印象深刻的东西(尽管对我来说它证实了 Linq-to-SQL 的缺点)
  • 我收到了The LINQ expression node type 'Invoke' is not supported in LINQ to Entities.。这段代码有什么限制吗?我想通过 IQueryables 执行 FULL JOIN
  • 我添加了一个新答案,用自定义 ExpressionVisitor 替换 Invoke 以内联 Invoke,因此它应该与 EF 一起使用。可以试试吗?
【解决方案11】:

我决定将此作为单独的答案添加,因为我不肯定它已经过足够的测试。这是FullOuterJoin 方法的重新实现,基本上使用了LINQKit Invoke/Expand 的简化定制版本Expression,以便它可以在实体框架中工作。没有太多解释,和我之前的回答差不多。

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 进行测试,所以 EF 6 不容易完成。 base.Visit(node) 不应抛出异常,因为它只会沿树递归。我可以访问几乎任何代码共享服务,但不能设置测试数据库。不过,针对我的 LINQ to SQL 测试运行它似乎工作正常。
  • @Troncho 您是否有可能在 Guid 键和 Guid? 外键之间加入?
  • 我也在使用 LinqPad 进行测试。我的查询引发了 ArgumentException,因此我决定在 [.Net Framework 4.7.1] 和最新的 EF 6 上的 VS2019 上对其进行调试。在那里我必须追踪真正的问题。为了测试您的代码,我生成了来自同一个 [Persons] 表的 2 个单独的数据集。我过滤了两个集合,以便某些记录对于每个集合都是唯一的,而一些记录在两个集合中都存在。 [PersonId] 是 [Primary Key] Guid (c#) / Uniqueidentifier (SqlServer),两者都没有生成任何 null [PersonId] 值。共享代码:github.com/Troncho/EF_FullOuterJoin
  • 当我将它与另一个类“无法创建类型为“TestProject.Contollers.TableViewModel”的空常量值一起使用时出现此错误。此上下文仅支持实体类型、枚举类型或原始类型。'
【解决方案12】:

两个或多个表的完全外连接: 首先提取您要加入的列。

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

【讨论】:

    【解决方案13】:

    对于密钥在两个枚举中都是唯一的情况,我的干净解决方案:

     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
    

    【讨论】:

      【解决方案14】:

      我认为 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
      

      【讨论】:

        【解决方案15】:

        感谢大家的有趣帖子!

        我修改了代码,因为在我的情况下我需要

        • 个性化连接谓词
        • 个性化联合不同比较器

        对于感兴趣的人,这是我修改后的代码(在 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
        

        【讨论】:

          【解决方案16】:

          又一个完全外连接

          由于对其他命题的简单性和可读性不太满意,我最终得到了这个:

          它没有快速的自负(在 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);
              }
          }
          

          }

          【讨论】:

            猜你喜欢
            • 2011-01-06
            • 2015-03-31
            • 2017-08-04
            • 1970-01-01
            • 2021-11-17
            • 1970-01-01
            相关资源
            最近更新 更多