C# Enumerable类 之 数据连接
总目录
前言
在 C# 中,System.Linq.Enumerable 类是 LINQ(Language Integrated Query)的核心组成部分,它提供了一系列静态方法,用于操作实现了 IEnumerable 接口的集合。通过这些方法,我们可以轻松地对集合进行查询、转换、排序和聚合等操作。
本文属于 C# Enumerable类 使用详解 中的一个章节,着重介绍 C# Enumerable 类中数据连接这部分的内容。
一、概览
方法 | 描述 |
---|---|
Join | 数据内连接 |
GroupJoin | 数据左外连接 |
二、Join
:数据内连接
1. 什么是 Join
Join
是 LINQ 提供的一个扩展方法,用于执行类似于 SQL 中的内连接(INNER JOIN)操作。它允许你根据指定的键选择器函数将两个序列中的元素进行匹配,并返回一个包含匹配结果的新序列。每个匹配的结果是一个匿名类型或自定义类型的对象,通常包含来自两个序列的相关数据。
2. Join方法 基本信息
1) Join
Join
方法的基本签名如下:
public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(
this IEnumerable<TOuter> outer,
IEnumerable<TInner> inner,
Func<TOuter, TKey> outerKeySelector,
Func<TInner, TKey> innerKeySelector,
Func<TOuter, TInner, TResult> resultSelector
)
public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(
this IEnumerable<TOuter> outer,
IEnumerable<TInner> inner,
Func<TOuter, TKey> outerKeySelector,
Func<TInner, TKey> innerKeySelector,
Func<TOuter, TInner, TResult> resultSelector,
IEqualityComparer<TKey> comparer
)
- 参数:
- outer:第一个要连接的序列。
- inner:第二个要连接的序列。
- outerKeySelector:一个函数,用于从
outer
序列的每个元素中提取键值。 - innerKeySelector:一个函数,用于从
inner
序列的每个元素中提取键值。 - resultSelector:一个函数,用于定义如何将匹配的
outer
和inner
元素组合成结果。 - comparer(可选):一个
IEqualityComparer<TKey>
实现,用于比较键值是否相等。
- 返回值:
Join
方法返回一个IEnumerable<TResult>
对象,其中TResult
是你通过结果选择器函数定义的输出类型。
2)工作原理
Join
方法根据指定的键选择器函数将两个序列中的元素进行匹配,并返回一个包含匹配结果的新序列。每个匹配的结果是一个由结果选择器函数定义的对象,通常包含来自两个序列的相关数据。类似于SQL中的内连接(INNER JOIN),只有当两个序列中存在匹配键时,才会生成输出结果。
3)使用场景
- 适用于数据关联、数据预处理、多个集合的连接等场景。
3. 使用示例
示例 1:基本连接操作
假设我们有两个列表,一个包含学生信息,另一个包含课程信息,我们希望根据学生的ID和课程的StudentID进行连接。
class Student
{
public int Id { get; set; }
public string Name { get; set; }
public override string ToString()
{
return $"{Id}: {Name}";
}
}
class Course
{
public int StudentId { get; set; }
public string CourseName { get; set; }
public override string ToString()
{
return $"{StudentId}: {CourseName}";
}
}
class Program
{
static void Main()
{
List<Student> students = new List<Student>
{
new Student { Id = 1, Name = "Alice" },
new Student { Id = 2, Name = "Bob" },
new Student { Id = 3, Name = "Charlie" }
};
List<Course> courses = new List<Course>
{
new Course { StudentId = 1, CourseName = "Math" },
new Course { StudentId = 2, CourseName = "Science" },
new Course { StudentId = 1, CourseName = "History" }
};
// 使用 Join 进行连接操作
var joinedData = students.Join(
courses,
student => student.Id, // 外部序列的键选择器
course => course.StudentId, // 内部序列的键选择器
(student, course) => new // 结果选择器
{
StudentName = student.Name,
CourseName = course.CourseName
});
// 输出结果
Console.WriteLine("Joined data:");
foreach (var item in joinedData)
{
Console.WriteLine($"Student: {item.StudentName}, Course: {item.CourseName}");
}
}
}
输出结果:
Joined data:
Student: Alice, Course: Math
Student: Alice, Course: History
Student: Bob, Course: Science
在这个例子中,我们使用 Join
方法根据学生的 Id
和课程的 StudentId
进行连接,并打印了结果。
using System;
using System.Collections.Generic;
using System.Linq;
class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public override string ToString()
{
return $"{Id}: {Name}";
}
}
class Order
{
public int CustomerId { get; set; }
public string Product { get; set; }
public override string ToString()
{
return $"{CustomerId}: {Product}";
}
}
class Program
{
static void Main()
{
List<Customer> customers = new List<Customer>
{
new Customer { Id = 1, Name = "Alice" },
new Customer { Id = 2, Name = "Bob" },
new Customer { Id = 3, Name = "Charlie" }
};
List<Order> orders = new List<Order>
{
new Order { CustomerId = 1, Product = "Apple" },
new Order { CustomerId = 2, Product = "Banana" },
new Order { CustomerId = 1, Product = "Cherry" }
};
// 使用 Join 关联客户和订单
var associatedData = customers.Join(
orders,
customer => customer.Id,
order => order.CustomerId,
(customer, order) => new
{
CustomerName = customer.Name,
Product = order.Product
});
// 输出结果
Console.WriteLine("Associated data:");
foreach (var item in associatedData)
{
Console.WriteLine($"Customer: {item.CustomerName}, Product: {item.Product}");
}
}
}
输出结果:
Associated data:
Customer: Alice, Product: Apple
Customer: Alice, Product: Cherry
Customer: Bob, Product: Banana
示例 2:自定义比较器
有时你需要自定义比较逻辑来判断两个键是否相等。这时可以传递一个实现了 IEqualityComparer<TKey>
接口的对象给 Join
方法。
class Student
{
public int Id { get; set; }
public string Name { get; set; }
public override string ToString()
{
return $"{Id}: {Name}";
}
}
class Course
{
public int StudentId { get; set; }
public string CourseName { get; set; }
public override string ToString()
{
return $"{StudentId}: {CourseName}";
}
}
class CustomComparer : IEqualityComparer<int>
{
public bool Equals(int x, int y)
{
// 自定义比较逻辑:允许Id相差不超过1 视为相同
return Math.Abs(x - y) <= 1;
}
public int GetHashCode(int obj)
{
return 0;
}
}
class Program
{
static void Main()
{
List<Student> students = new List<Student>
{
new Student { Id = 1, Name = "Alice" },
new Student { Id = 4, Name = "Bob" },
new Student { Id = 7, Name = "Charlie" }
};
List<Course> courses = new List<Course>
{
new Course { StudentId = 2, CourseName = "Math" },
new Course { StudentId = 4, CourseName = "Science" },
new Course { StudentId = 9, CourseName = "History" } // 注意这里的StudentId为4
};
// 使用 Join 进行连接操作并使用自定义比较器
var joinedData = students.Join(
courses,
student => student.Id,
course => course.StudentId,
(student, course) => new
{
StudentName = student.Name,
CourseName = course.CourseName
},
new CustomComparer());
// 输出结果
Console.WriteLine("Joined data with custom comparer:");
foreach (var item in joinedData)
{
Console.WriteLine($"Student: {item.StudentName}, Course: {item.CourseName}");
}
}
}
输出结果:
Joined data with custom comparer:
Student: Alice, Course: Math
Student: Bob, Course: Science
在这个例子中,我们展示了如何使用自定义比较器来进行连接操作,并确保比较是基于自定义逻辑进行的。
示例 3:惰性求值示例
Join
方法采用惰性求值(Lazy Evaluation),这意味着它不会立即执行连接操作,而是等到实际遍历时才会计算结果。这使得 Join
可以处理无限序列或延迟执行复杂的查询。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
IEnumerable<int> infiniteNumbers = Enumerable.Range(0, int.MaxValue).Select(i => i * 2);
List<int> finiteNumbers = new List<int> { 4, 6, 8 };
// 使用 Join 进行连接操作
var joinedSequence = infiniteNumbers.Join(
finiteNumbers,
number => number,
target => target,
(number, target) => new { Number = number, Target = target });
// 限制输出数量
Console.WriteLine("Joined elements (First 3 elements):");
foreach (var item in joinedSequence.Take(3))
{
Console.WriteLine($"Number: {item.Number}, Target: {item.Target}");
}
}
}
输出结果:
Joined elements (First 3 elements):
Number: 4, Target: 4
Number: 6, Target: 6
Number: 8, Target: 8
在这个例子中,我们展示了如何使用 Join
方法计算一个无限序列和一个有限序列之间的连接,并通过 Take
方法限制输出的数量,从而避免无限循环。
示例 4: 多个集合的连接
当需要计算多个集合的连接时,可以多次使用 Join
方法,或者使用更复杂的数据结构和查询逻辑来简化代码。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<int> list1 = new List<int> { 1, 2, 3, 4, 5 };
List<int> list2 = new List<int> { 3, 4, 6 };
List<int> list3 = new List<int> { 4, 7, 8 };
// 使用 Join 计算多个集合的连接
var joinedList = list1.Join(
list2,
number => number,
target => target,
(number, target) => new { Number = number, Target = target })
.Join(
list3,
firstJoin => firstJoin.Target,
thirdList => thirdList,
(firstJoin, thirdList) => new { FirstJoin = firstJoin, ThirdList = thirdList });
// 输出结果
Console.WriteLine("Joined data among list1, list2, and list3:");
foreach (var item in joinedList)
{
Console.WriteLine($"Number: {item.FirstJoin.Number}, Target: {item.FirstJoin.Target}, ThirdList: {item.ThirdList}");
}
}
}
输出结果:
Joined data among list1, list2, and list3:
Number: 4, Target: 4, ThirdList: 4
二、GroupJoin
:数据类型转换
1. 什么是 GroupJoin
GroupJoin
是 LINQ 提供的一个扩展方法,用于执行类似于 SQL 中的左外连接(LEFT OUTER JOIN)操作。它允许你根据指定的键选择器函数将两个序列中的元素进行匹配,并返回一个包含每个外部元素及其相关内部元素集合的结果。每个匹配的结果是一个匿名类型或自定义类型的对象,通常包含来自两个序列的相关数据。
2. GroupJoin 方法 基本信息
1) GroupJoin
GroupJoin
方法的基本签名如下:
public static IEnumerable<TResult> GroupJoin<TOuter, TInner, TKey, TResult>(
this IEnumerable<TOuter> outer,
IEnumerable<TInner> inner,
Func<TOuter, TKey> outerKeySelector,
Func<TInner, TKey> innerKeySelector,
Func<TOuter, IEnumerable<TInner>, TResult> resultSelector
)
public static IEnumerable<TResult> GroupJoin<TOuter, TInner, TKey, TResult>(
this IEnumerable<TOuter> outer,
IEnumerable<TInner> inner,
Func<TOuter, TKey> outerKeySelector,
Func<TInner, TKey> innerKeySelector,
Func<TOuter, IEnumerable<TInner>, TResult> resultSelector,
IEqualityComparer<TKey> comparer
)
- 参数:
- outer:第一个要连接的序列。
- inner:第二个要连接的序列。
- outerKeySelector:一个函数,用于从
outer
序列的每个元素中提取键值。 - innerKeySelector:一个函数,用于从
inner
序列的每个元素中提取键值。 - resultSelector:一个函数,用于定义如何将外部元素和其相关的内部元素集合组合成结果。
- comparer(可选):一个
IEqualityComparer<TKey>
实现,用于比较键值是否相等。
- 返回值:
GroupJoin
方法返回一个IEnumerable<TOuter>
对象,其中每个TOuter
元素都包含一个与其相关的内部元素集合(IEnumerable<TInner>
)。
2)工作原理
GroupJoin
方法根据指定的键选择器函数将两个序列中的元素进行匹配,并返回一个包含匹配结果的新序列。与 Join 不同的是,GroupJoin 对于第一个序列中的每个元素,返回第二个序列中所有具有相同键的元素组成的组,类似于SQL中的左外连接(LEFT OUTER JOIN),但返回的结果是分组形式,允许访问每个组中的所有相关项。
两种方法的核心功能和主要区别:
Join
主要用于找到两个序列之间的匹配对,并生成一个新的结果序列。GroupJoin
则更进一步,它不仅找到匹配对,还保留了每个匹配对的分组信息,适用于需要处理一对多关系的场景。
3. 使用示例
示例 1:基本分组连接操作
假设我们有两个列表,一个包含学生信息,另一个包含课程信息,我们希望根据学生的ID和课程的StudentID进行分组连接。
class Student
{
public int Id { get; set; }
public string Name { get; set; }
public override string ToString()
{
return $"{Id}: {Name}";
}
}
class Course
{
public int StudentId { get; set; }
public string CourseName { get; set; }
public override string ToString()
{
return $"{StudentId}: {CourseName}";
}
}
class Program
{
static void Main()
{
List<Student> students = new List<Student>
{
new Student { Id = 1, Name = "Alice" },
new Student { Id = 2, Name = "Bob" },
new Student { Id = 3, Name = "Charlie" }
};
List<Course> courses = new List<Course>
{
new Course { StudentId = 1, CourseName = "Math" },
new Course { StudentId = 2, CourseName = "Science" },
new Course { StudentId = 1, CourseName = "History" }
};
// 使用 GroupJoin 进行分组连接操作
var groupedData = students.GroupJoin(
courses,
student => student.Id, // 外部序列的键选择器
course => course.StudentId, // 内部序列的键选择器
(student, studentCourses) => new // 结果选择器
{
StudentName = student.Name,
Courses = studentCourses.Select(c => c.CourseName).ToList()
});
// 输出结果
Console.WriteLine("Grouped data:");
foreach (var item in groupedData)
{
Console.Write($"Student: {item.StudentName}, Courses: ");
if (item.Courses.Any())
{
Console.WriteLine(string.Join(", ", item.Courses));
}
else
{
Console.WriteLine("None");
}
}
}
}
输出结果:
Grouped data:
Student: Alice, Courses: Math, History
Student: Bob, Courses: Science
Student: Charlie, Courses: None
在这个例子中,我们使用 GroupJoin
方法根据学生的 Id
和课程的 StudentId
进行分组连接,并打印了结果。
示例 2:自定义比较器
有时你需要自定义比较逻辑来判断两个键是否相等。这时可以传递一个实现了 IEqualityComparer<TKey>
接口的对象给 GroupJoin
方法。
可参考 Join
方法中案例。
示例 3:惰性求值
GroupJoin
方法采用惰性求值(Lazy Evaluation),这意味着它不会立即执行连接操作,而是等到实际遍历时才会计算结果。这使得 GroupJoin
可以处理无限序列或延迟执行复杂的查询。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
IEnumerable<int> infiniteNumbers = Enumerable.Range(0, int.MaxValue).Select(i => i * 2);
List<int> finiteNumbers = new List<int> { 4, 6, 8 };
// 使用 GroupJoin 进行分组连接操作
var groupedSequence = infiniteNumbers.GroupJoin(
finiteNumbers,
number => number,
target => target,
(number, targets) => new { Number = number, Targets = targets.ToList() });
// 限制输出数量
Console.WriteLine("Grouped elements (First 3 elements):");
foreach (var item in groupedSequence.Take(3))
{
Console.Write($"Number: {item.Number}, Targets: ");
if (item.Targets.Any())
{
Console.WriteLine(string.Join(", ", item.Targets));
}
else
{
Console.WriteLine("None");
}
}
}
}
输出结果:
Grouped elements (First 3 elements):
Number: 4, Targets: 4
Number: 6, Targets: 6
Number: 8, Targets: 8
在这个例子中,我们展示了如何使用 GroupJoin
方法计算一个无限序列和一个有限序列之间的分组连接,并通过 Take
方法限制输出的数量,从而避免无限循环。
示例 4:多个集合的分组连接
当需要计算多个集合的分组连接时,可以多次使用 GroupJoin
方法,或者使用更复杂的数据结构和查询逻辑来简化代码。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<int> list1 = new List<int> { 1, 2, 3, 4, 5 };
List<int> list2 = new List<int> { 3, 4, 6 };
List<int> list3 = new List<int> { 4, 7, 8 };
// 使用 GroupJoin 计算多个集合的分组连接
var groupedList = list1.GroupJoin(
list2,
number => number,
target => target,
(number, targets) => new { Number = number, Targets = targets.ToList() })
.GroupJoin(
list3,
firstGroup => firstGroup.Number,
thirdList => thirdList,
(firstGroup, thirdTargets) => new
{
FirstGroup = firstGroup,
ThirdTargets = thirdTargets.ToList()
});
// 输出结果
Console.WriteLine("Grouped data among list1, list2, and list3:");
foreach (var item in groupedList)
{
Console.Write($"Number: {item.FirstGroup.Number}, Targets: ");
if (item.FirstGroup.Targets.Any())
{
Console.Write(string.Join(", ", item.FirstGroup.Targets));
}
else
{
Console.Write("None");
}
Console.Write(", ThirdTargets: ");
if (item.ThirdTargets.Any())
{
Console.WriteLine(string.Join(", ", item.ThirdTargets));
}
else
{
Console.WriteLine("None");
}
}
}
}
输出结果:
Grouped data among list1, list2, and list3:
Number: 1, Targets: None, ThirdTargets: None
Number: 2, Targets: None, ThirdTargets: None
Number: 3, Targets: 3, ThirdTargets: None
Number: 4, Targets: 4, ThirdTargets: 4
Number: 5, Targets: None, ThirdTargets: None
结语
回到目录页:C#/.NET 知识汇总
希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。
参考资料:
微软官方文档 Enumerable