參考資料:
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
- 測試數據准備
- Join
- 多條件Join
- 自定義
IEqualityComparer<T>
的Join - GroupJoin
- 多條件GroupJoin
- 自定義
IEqualityComparer<T>
的GroupJoin
測試數據准備
先准備一下測試數據。建一個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的情況基本相同,不再舉例。