Linq的Join与GroupJoin详解

参考资料:

MSDN官方文档:

https://docs.microsoft.com/zh-cn/dotnet/api/system.linq.enumerable.join?view=net-5.0

https://docs.microsoft.com/zh-cn/dotnet/api/system.linq.enumerable.groupjoin?view=net-5.0

https://docs.microsoft.com/zh-cn/dotnet/api/system.collections.generic.iequalitycomparer-1?view=net-5.0

测试数据准备

先准备一下测试数据。建一个Person类和Country类,每个Person都有一个Country,通过Person的CountryId属性和Country的Id属性关联。准备用他们两个类搞两个列表,进行Join操作:

class Person
{
    public Person(int id, string name, Gender gender, int age, int iQ, int fQ, int countryId)
    {
        Id = id;
        Name = name;
        Gender = gender;
        Age = age;
        IQ = iQ;
        FQ = fQ;
        CountryId = countryId;
    }

    public override string ToString()
    {
        return $"Name: {Name}, Gender: {Gender}, Age: {Age}, IQ: {IQ}, FQ: {FQ}";
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public Gender Gender { get; set; }
    public int Age { get; set; }
    public int IQ { get; set; } // 智力
    public int FQ { get; set; } // 武力
    public int CountryId { get; set; }
}

// 性别的枚举
enum Gender
{
    Male = 1,
    Female = 2
}

class Country
{

    public Country(int id, string name, int leaderId)
    {
        Id = id;
        Name = name;
        LeaderId = leaderId;
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public int LeaderId { get; set; }
}

然后用两个类构建两个List:

var countryList = new List<Country> {
    new Country(1, "蜀", 1),
    new Country(2, "魏", 2),
    new Country(3, "吴", 3),
};

var list = new List<Person> {
    new Person(1, "刘备", Gender.Male, 41, 90, 80, 1),
    new Person(2, "曹操", Gender.Male, 42, 90, 80, 2),
    new Person(3, "孙权", Gender.Male, 20, 70, 70, 3),
    new Person(4, "关羽", Gender.Male, 35, 90, 100, 1),
    new Person(5, "张飞", Gender.Male, 30, 80, 98, 1),
    new Person(6, "夏侯惇", Gender.Male, 35, 75, 97, 2),
    new Person(7, "夏侯渊", Gender.Male, 30, 80, 95, 2),
    new Person(8, "周瑜", Gender.Male, 27, 99, 80, 3),
    new Person(9, "太史慈", Gender.Male, 38, 80, 97, 3)
};

Join

Join有两个重载,第一种是“基于匹配键对两个序列的元素进行关联。 使用默认的相等比较器对键进行比较。”,类似与SQL中,JOIN语句on后面的比较条件是两张表进行联结的字段“相等”。

将两个list通过Country的Id来Join起来,最终取每个人的Name,Gender和他所在的Country的Name:

var queryJoin = list.Join(
    inner: countryList,
    outerKeySelector: l => l.CountryId,
    innerKeySelector: c => c.Id,
    resultSelector: (l, c) => new { Name = l.Name, Gender = l.Gender, Country = c.Name }
    );

queryJoin.ToList().ForEach(q => Console.WriteLine($"Name: {q.Name}, Gender: {q.Gender}, Country: {q.Country}"));

// 运行结果:
//Name: 刘备, Gender: Male, Country: 蜀
//Name: 曹操, Gender: Male, Country: 魏
//Name: 孙权, Gender: Male, Country: 吴
//Name: 关羽, Gender: Male, Country: 蜀
//Name: 张飞, Gender: Male, Country: 蜀
//Name: 夏侯惇, Gender: Male, Country: 魏
//Name: 夏侯渊, Gender: Male, Country: 魏
//Name: 周瑜, Gender: Male, Country: 吴
//Name: 太史慈, Gender: Male, Country: 吴

这个Join方法的声明如下:

public static System.Collections.Generic.IEnumerable<TResult> Join<TOuter,TInner,TKey,TResult> (
    this System.Collections.Generic.IEnumerable<TOuter> outer, 
    System.Collections.Generic.IEnumerable<TInner> inner, 
    Func<TOuter,TKey> outerKeySelector, 
    Func<TInner,TKey> innerKeySelector, 
    Func<TOuter,TInner,TResult> resultSelector);

我们看形参的参数名来分析一下形参:

  • outer是调用Join方法的IEnumerable<T>类型的对象的本身,在上面例子中就是list。也就是被联接的对象。在这个例子中。
  • inner是联接的IEnumerable<T>类型的对象,在上面例子中是countryList。在这个例子中,TOuter与TInner两个泛型的类型也已经确定了,分别是Person和Country。
  • outerKeySelector是一个Func委托,从参数名来看,这个委托的参数类型显然要是上面的TOuter,在这个例子中也就是Person。委托的返回值类型是TKey,与下面innerKeySelector委托的返回值类型应当是一样的。在上面例子中他们分别是Person的CountryId和Country的Id,都是int类型的。
  • innerKeySelector是一个Func委托,这个委托的参数类型显然要是上面的TInner,在这个例子中也就是Country。剩下的上一条已经介绍过了。outerKeySelector和innerKeySelector就是作为这次Join操作的on的条件,要进行比较的一对值。当然也可以是多对,可以通过匿名类进行构造,下面会有例子介绍。
  • resultSelector是一个Func委托,两个参数分别是TOuter与TInner类型,会有返回值,就是进行这次Join操作之后,我们能获取到的值。可以是单个值,或者对象,或者Person和Country各取几个字段,用匿名类搞一个匿名对象。

多条件Join

使用Person.CountryId和Person.Id来Join Country.Id和Country.LeaderId,这是两对条件的Join,最终实际获取到的应该是三个国家的Leader的信息:

var queryJoin2 = list.Join(
    inner: countryList,
    outerKeySelector: l => new { CountryId = l.CountryId, LeaderId = l.Id },
    innerKeySelector: c => new { CountryId = c.Id, LeaderId = c.LeaderId },
    resultSelector: (l, c) => new { Name = l.Name, Gender = l.Gender, Country = c.Name }
    );

queryJoin2.ToList().ForEach(q => Console.WriteLine($"Name: {q.Name}, Gender: {q.Gender}, Country: {q.Country}"));

// 执行结果:
//Name: 刘备, Gender: Male, Country: 蜀
//Name: 曹操, Gender: Male, Country: 魏
//Name: 孙权, Gender: Male, Country: 吴

自定义IEqualityComparer<T>的Join

第二个重载多了一个参数,“基于匹配键对两个序列的元素进行关联。 使用指定的 IEqualityComparer<T> 对键进行比较。”,类似SQL中JOIN ON后面的条件不再是普通的值之间或者对象之间的相等,而是你传进去作为参数的一个相等比较器中定义的相等的规则。

这里我给list新增一个人,名为“汉昭烈帝”,增加后的list如下所示:

var list = new List<Person> {
    new Person(1, "刘备", Gender.Male, 41, 90, 80, 1),
    new Person(2, "曹操", Gender.Male, 42, 90, 80, 2),
    new Person(3, "孙权", Gender.Male, 20, 70, 70, 3),
    new Person(4, "关羽", Gender.Male, 35, 90, 100, 1),
    new Person(5, "张飞", Gender.Male, 30, 80, 98, 1),
    new Person(6, "夏侯惇", Gender.Male, 35, 75, 97, 2),
    new Person(7, "夏侯渊", Gender.Male, 30, 80, 95, 2),
    new Person(8, "周瑜", Gender.Male, 27, 99, 80, 3),
    new Person(9, "太史慈", Gender.Male, 38, 80, 97, 3),
    new Person(10, "汉昭烈帝", Gender.Male, 41, 90, 80, 1)
};

我现在认为Id和Name并不能作为区分Person不相同的条件,因为Id和名称都只是一个代号。比如曹操可能直呼刘备的名字,或者叫他“玄德”,而后朝人可能称呼刘备为“汉昭烈帝”或者“昭烈皇帝”。所以我认为刘备和汉昭烈帝应该是同一个人。

此处我设定:Id和Name不作为判断是否是同一个人的条件,除了这两个属性,Person剩下的所有属性共同作为判断两个人是否是同一个人的条件,即假设有两个Person(person1和person2),这两个Person除Id和Name外,剩下的属性全部相等,我就认为person1和person2是同一个人。我打算对list进行自联接,筛选出同一个人。

现在我们看一下包含自定义相等比较器(即IEqualityComparer<T>)的Join方法的声明:

public static System.Collections.Generic.IEnumerable<TResult> Join<TOuter,TInner,TKey,TResult> (
    this System.Collections.Generic.IEnumerable<TOuter> outer, 
    System.Collections.Generic.IEnumerable<TInner> inner, 
    Func<TOuter,TKey> outerKeySelector, 
    Func<TInner,TKey> innerKeySelector, 
    Func<TOuter,TInner,TResult> resultSelector, 
    System.Collections.Generic.IEqualityComparer<TKey>? comparer);

跟普通的Join相比,就多了一个可空的IEqualityComparer<TKey>类型的参数。

此处我们需要自定义一个实现了IEqualityComparer<Person>的类,实现该接口,必须实现Equals和GetHashCode这两个方法:

class PersonEqualityComparer : IEqualityComparer<Person>
{
    public bool Equals(Person x, Person y)
    {
        if (x == null && y == null)
        {
            return true;
        }
        else if (x == null || y == null)
        {
            return false;
        }
        else if (x.Gender == y.Gender && x.Age == y.Age && x.IQ == y.IQ && x.FQ == y.FQ && x.CountryId == y.CountryId)
        {
            return true;
        }
        else
        {
            return false;
        }
    }

    public int GetHashCode([DisallowNull] Person obj)
    {
        int hCode = ((int)obj.Gender) ^ obj.Age ^ obj.IQ ^ obj.FQ ^ obj.CountryId;
        return hCode.GetHashCode();
    }
}

然后就可以声明一个PersonEqualityComparer类型的对象,作为相等比较器,用于Join操作。对list进行自联接,筛选出同一个人:

var personEqualComparer = new PersonEqualityComparer(); // 自定义的Person相等比较器

var queryJoin3 = list.Join(
    inner: list,
    outerKeySelector: person1 => person1,
    innerKeySelector: person2 => person2,
    resultSelector: (p1, p2) => new { L1Name = p1.Name, L2Name = p2.Name },
    comparer: personEqualComparer // 使用自定义的Person相等比较器对象
    ).Where(res => res.L1Name != res.L2Name); // 筛选掉Join过程中因为自己等于自己而联接的那些无效的数据

queryJoin3.ToList().ForEach(p => Console.WriteLine(p));

// 运行结果:
//{ L1Name = 刘备, L2Name = 汉昭烈帝 }
//{ L1Name = 汉昭烈帝, L2Name = 刘备 }

GroupJoin

GroupJoin与Join差不多,这里不再做太多的举例。它也有两个重载,第一种是“基于键值等同性对两个序列的元素进行关联,并对结果进行分组。 使用默认的相等比较器对键进行比较。”:(下面都是将测试数据中添加的“汉昭烈帝”删掉之后的运行结果)

// 根据国家分组,需要用countryList来联接list
var queryGroupJoin = countryList.GroupJoin(
    inner: list,
    outerKeySelector: c => c.Id,
    innerKeySelector: l => l.CountryId,
    resultSelector: (c, lCollection) => new { Country = c, Persons = lCollection }
    );

queryGroupJoin.ToList().ForEach(q =>
{
    Console.WriteLine("Country: " + q.Country.Name);
    q.Persons.ToList().ForEach(p => Console.WriteLine(p));
    Console.WriteLine();
});

// 运行结果:
//Country: 蜀
//Name: 刘备, Gender: Male, Age: 41, IQ: 90, FQ: 80
//Name: 关羽, Gender: Male, Age: 35, IQ: 90, FQ: 100
//Name: 张飞, Gender: Male, Age: 30, IQ: 80, FQ: 98

//Country: 魏
//Name: 曹操, Gender: Male, Age: 42, IQ: 90, FQ: 80
//Name: 夏侯惇, Gender: Male, Age: 35, IQ: 75, FQ: 97
//Name: 夏侯渊, Gender: Male, Age: 30, IQ: 80, FQ: 95

//Country: 吴
//Name: 孙权, Gender: Male, Age: 20, IQ: 70, FQ: 70
//Name: 周瑜, Gender: Male, Age: 27, IQ: 99, FQ: 80
//Name: 太史慈, Gender: Male, Age: 38, IQ: 80, FQ: 97

可以看到它跟Join的区别是resultSelector的第二个参数变成了Person的一个集合IEnumerable<Person>,这也是分组的结果,这个集合就是按Country分组后,每个Country下的Person。

当然还可以在GroupJoin中做一些排序,筛选之类的操作:

// 最终获取的Persons按照年龄降序排列,且只取Name
var queryGroupJoin2 = countryList.GroupJoin(
    inner: list,
    outerKeySelector: c => c.Id,
    innerKeySelector: l => l.CountryId,
    resultSelector: (c, lCollection) => new { Country = c.Name, Persons = lCollection.OrderByDescending(p => p.Age).Select(p => p.Name) }
    );
queryGroupJoin2.ToList().ForEach(q => Console.WriteLine($"Country: {q.Country}, Person: {string.Join(" & ", q.Persons)}"));

// 运行结果:
//Country: 蜀, Person: 刘备 & 关羽 & 张飞
//Country: 魏, Person: 曹操 & 夏侯惇 & 夏侯渊
//Country: 吴, Person: 太史慈 & 周瑜 & 孙权

多条件GroupJoin

依旧使用Person.CountryId和Person.Id来Join Country.Id和Country.LeaderId,这是两对条件的Join,最终实际获取到的应该是三个国家的Leader的信息,而一国无二主(唐高宗和武则天共同治国时期除外),也就是说每个分组中都只有一个Person,就是这个国家的Leader:

var queryGroupJoin3 = countryList.GroupJoin(
    inner: list,
    outerKeySelector: c => new { CountryId = c.Id, LeaderId = c.LeaderId },
    innerKeySelector: l => new { CountryId = l.CountryId, LeaderId = l.Id },
    resultSelector: (c, lCollection) => new { Country = c, Persons = lCollection }
    );

queryGroupJoin3.ToList().ForEach(q =>
{
    Console.WriteLine("Country: " + q.Country.Name);
    q.Persons.ToList().ForEach(p => Console.WriteLine(p));
    Console.WriteLine();
});

// 运行结果:
//Country: 蜀
//Name: 刘备, Gender: Male, Age: 41, IQ: 90, FQ: 80

//Country: 魏
//Name: 曹操, Gender: Male, Age: 42, IQ: 90, FQ: 80

//Country: 吴
//Name: 孙权, Gender: Male, Age: 20, IQ: 70, FQ: 70

自定义IEqualityComparer<T>的GroupJoin

与Join的情况基本相同,不再举例。

原文地址:https://www.cnblogs.com/Kit-L/p/14839434.html