C#Linq查询(九):连接与分组(纯干货)

九、连接与分组

用于关联多个序列(如按键匹配两个集合)或按键分组(将同一键的元素归类),是处理多源数据关系和聚合分析的核心工具,类似数据库中的 “连接” 和 “分组” 操作。

1. Zip(second, resultSelector):按索引配对序列

将两个序列按索引一一配对(如第一个序列的第 n 个元素与第二个序列的第 n 个元素配对),通过resultSelector生成新元素。若序列长度不同,以较短序列为准(忽略较长序列的多余元素)。

性能特性:O (n) 复杂度(n 为较短序列的长度),按索引顺序遍历配对。

典型场景:合并两个长度相同的序列(如 “姓名” 与 “成绩” 配对生成 “学生信息”)、对应元素计算(如两列数值相加)。

示例 1:基础数值配对
var numbers1 = new List<int> { 1, 2, 3 };
var numbers2 = new List<int> { 10, 20, 30 };

// 对应索引元素相加(1+10, 2+20, 3+30)
IEnumerable<int> sums = numbers1.Zip(numbers2, (x, y) => x + y); // {11, 22, 33}
示例 2:对象序列配对
var names = new List<string> { "张三", "李四", "王五" };
var ages = new List<int> { 25, 30, 28 };

// 配对生成“姓名-年龄”字符串
IEnumerable<string> userInfo = names.Zip(ages, (name, age) => $"{name}{age}岁)");
// 结果:{"张三(25岁)", "李四(30岁)", "王五(28岁)"}
示例 3:处理长度不匹配的序列
var listA = new List<int> { 1, 2 };
var listB = new List<int> { 10, 20, 30 }; // 更长的序列

// 仅配对前2个元素(以较短序列为准)
IEnumerable<int> zipped = listA.Zip(listB, (a, b) => a * b); // {10, 40}(忽略30)

2. Join(inner, outerKeySelector, innerKeySelector, resultSelector):内连接

类似 SQL 的 INNER JOIN,根据键匹配两个序列(outer 为外部序列,inner 为内部序列),返回匹配的元素组合。仅保留两边都有匹配键的元素。

核心参数

  • outerKeySelector:从外部序列元素中提取用于匹配的键。
  • innerKeySelector:从内部序列元素中提取用于匹配的键。
  • resultSelector:定义匹配后生成的结果(如合并两个元素的属性)。

性能特性:内部通过哈希表匹配键(O (n + m) 复杂度,n 和 m 为两个序列的长度),比嵌套循环更高效。

典型场景:关联两个集合中 “键相同” 的数据(如 “订单” 与 “用户” 通过 “用户 ID” 关联)。

示例:订单与用户内连接
// 外部序列:订单
public class Order {
    public int Id { get; set; }
    public int UserId { get; set; } // 关联用户的键
    public decimal Amount { get; set; }
}

// 内部序列:用户
public class User {
    public int Id { get; set; } // 被关联的键
    public string Name { get; set; }
}

var orders = new List<Order> {
    new Order { Id = 1, UserId = 101, Amount = 199 },
    new Order { Id = 2, UserId = 102, Amount = 299 },
    new Order { Id = 3, UserId = 103, Amount = 99 } // UserId=103在用户表中无匹配
};

var users = new List<User> {
    new User { Id = 101, Name = "张三" },
    new User { Id = 102, Name = "李四" }
};

// 内连接:按UserId匹配订单和用户(仅保留两边都有匹配的记录)
var joined = orders.Join(
    inner: users,
    outerKeySelector: o => o.UserId, // 订单的匹配键
    innerKeySelector: u => u.Id,     // 用户的匹配键
    resultSelector: (order, user) => new { // 合并结果
        OrderId = order.Id,
        UserName = user.Name,
        Amount = order.Amount
    }
);

// 结果:仅包含UserId=101和102的订单(排除103)
// { {OrderId=1, UserName="张三", Amount=199}, {OrderId=2, UserName="李四", Amount=299} }

3. GroupJoin(inner, outerKeySelector, innerKeySelector, resultSelector):左外连接

类似 SQL 的 LEFT JOIN,返回外部序列的所有元素,以及内部序列中匹配的元素集合(若无匹配,返回空集合)。

Join的区别

  • Join 返回 “一对一” 的匹配组合(每个匹配生成一个结果)。
  • GroupJoin 返回 “一对多” 的匹配组合(外部元素对应一组内部匹配元素)。

典型场景:获取 “外部元素及其所有关联的内部元素”(如 “用户及其所有订单”,包括无订单的用户)。

示例:用户与订单左外连接
// 复用上面的Order和User类
var users = new List<User> {
    new User { Id = 101, Name = "张三" },
    new User { Id = 102, Name = "李四" },
    new User { Id = 104, Name = "赵六" } // 无匹配订单
};

var orders = new List<Order> {
    new Order { Id = 1, UserId = 101, Amount = 199 },
    new Order { Id = 2, UserId = 101, Amount = 299 }, // 张三的第二个订单
    new Order { Id = 3, UserId = 102, Amount = 99 }
};

// 左外连接:用户及其所有订单(包括无订单的用户)
var groupJoined = users.GroupJoin(
    inner: orders,
    outerKeySelector: u => u.Id,     // 用户的匹配键
    innerKeySelector: o => o.UserId, // 订单的匹配键
    resultSelector: (user, userOrders) => new { // 合并结果
        UserName = user.Name,
        Orders = userOrders.Select(o => new { o.Id, o.Amount }), // 该用户的所有订单
        TotalAmount = userOrders.Sum(o => o.Amount) // 订单总金额(无订单则为0)
    }
);

// 结果:
// 1. 张三:2个订单,总金额498
// 2. 李四:1个订单,总金额99
// 3. 赵六:0个订单,总金额0

4. GroupBy(keySelector):按键分组

将序列中的元素按指定键分组,返回 IGrouping<TKey, TElement> 集合(每个分组包含一个键和该键对应的所有元素)。支持多级分组(嵌套GroupBy)和分组后聚合(如计数、求和)。

性能特性:O (n) 复杂度(通过哈希表分组),分组后可直接对组内元素进行聚合操作。

典型场景:数据分类统计(如 “按部门分组统计员工数量”“按月份分组统计订单金额”)。

示例 1:基础分组与聚合
var scores = new List<int> { 80, 90, 75, 85, 95, 70 };

// 按“是否及格”分组(键为bool类型)
var groupedByPass = scores.GroupBy(
    keySelector: s => s >= 60, // 分组键:是否及格
    elementSelector: s => s    // 组内元素(默认为原元素,可省略)
);

foreach (var group in groupedByPass) {
    Console.WriteLine($"键:{group.Key}{group.Count()}个元素)");
    Console.WriteLine($"元素:{string.Join(", ", group)}");
}
// 输出:
// 键:True(6个元素)
// 元素:80, 90, 75, 85, 95, 70
示例 2:对象分组与多级统计
public class Sales {
    public string Region { get; set; } // 地区
    public string Product { get; set; } // 产品
    public decimal Amount { get; set; } // 销售额
}

var sales = new List<Sales> {
    new Sales { Region = "华东", Product = "手机", Amount = 10000 },
    new Sales { Region = "华东", Product = "电脑", Amount = 15000 },
    new Sales { Region = "华北", Product = "手机", Amount = 8000 },
    new Sales { Region = "华北", Product = "手机", Amount = 5000 }
};

// 1. 先按地区分组,再按产品分组(多级分组)
var regionProductGroups = sales
    .GroupBy(s => s.Region) // 一级键:地区
    .Select(regionGroup => new {
        Region = regionGroup.Key,
        Products = regionGroup.GroupBy(p => p.Product) // 二级键:产品
            .Select(productGroup => new {
                Product = productGroup.Key,
                Total = productGroup.Sum(s => s.Amount) // 该产品在该地区的总销售额
            })
    });

// 2. 直接按地区分组并计算总销售额
var regionTotal = sales
    .GroupBy(s => s.Region)
    .ToDictionary(
        group => group.Key,
        group => group.Sum(s => s.Amount) // 地区总销售额
    );
// regionTotal 结果:{ "华东": 25000, "华北": 13000 }

核心对比与注意事项

方法核心逻辑输出类型典型场景
Zip按索引配对两个序列合并后的新序列对应元素计算、数据合并
Join内连接(仅保留匹配元素)一对一的匹配组合关联两个集合的匹配数据
GroupJoin左外连接(保留外部所有元素 + 匹配的内部元素组)一对多的匹配组合外部元素及其关联的内部元素集合
GroupBy按键分组元素IGrouping<TKey, TElement> 集合分类统计(如按部门 / 月份分组)

关键注意事项

  1. 键的相等性
    • Join/GroupJoin/GroupBy 依赖键的相等性,自定义键需确保正确实现 GetHashCodeEquals,或通过 IEqualityComparer<T> 指定比较规则。
    • 示例:按字符串键分组时忽略大小写,可传入 StringComparer.OrdinalIgnoreCase
  2. JoinGroupJoin 的选择
    • 需 “一对一” 结果(如 “订单 + 用户” 的扁平结构):用 Join
    • 需 “一对多” 结果(如 “用户 + 其所有订单” 的嵌套结构):用 GroupJoin
  3. GroupBy 的灵活性
    • 可通过 elementSelector 筛选组内元素(如仅保留需要的属性)。
    • 可通过 resultSelector 直接生成聚合结果(如 GroupBy(key, (k, g) => new { Key = k, Count = g.Count() }))。
  4. 性能优化
    • 大集合连接 / 分组时,确保键的哈希计算高效(避免复杂对象作为键)。
    • 分组后如需多次访问组内元素,建议转为 List 缓存(如 group.ToList())。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值