c# - LINQ - 完全外部连接

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

我有一个人的 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

我做错了什么?

最佳答案

更新 1:提供真正通用的扩展方法 FullOuterJoin
更新 2:可选择接受自定义 IEqualityComparer对于 key 类型
更新 3:此实现有 recently become part of 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 将重复 a 中的元素b 中元素的次数使用相应的键(即:如果 b 为空,则没有)。 数据库术语称之为 inner (equi)join
  • 外部联接 包含来自 a 的元素没有相应的 元素存在于b中. (即:即使 b 为空,结果也是如此)。 这通常称为 left join
  • 完全外部联接 包括来自 a 的记录以及 b 如果没有相应的元素存在于另一个。 (即,即使 a 为空,结果也是如此)

在 RDBMS 中通常看到的是组连接[1]:

  • 一个group join,和上面描述的一样,但是而不是重复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;
    }
}

关于c# - LINQ - 完全外部连接,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/5489987/

相关文章:

c# - C# 7 中 ValueTuple 的 KeyValuePair 命名

c# - 属性初始化反模式

c# - 使用 LINQ 从 List<T> 中删除元素

c# - 你如何将 LINQ 与 Sqlite 一起使用

c# - 如何使用 lambda 表达式和匿名类型获取类型的属性名称?

c# - 如何将重复的监视器检测为单独的屏幕

c# - 以编程方式检查 .NET Framework 版本 - 我应该这样做吗?

.net - 如何编写可与 SqlServer 和 Oracle 一起使用的 .Net 应用程序(现在 System.Data.OracleClient 已弃用)

c# - 枚举类模式是否与 DI 不兼容?

c# - 使用 LINQ 连接字符串