如何用LINQ按属性值拆分列表并保留原顺序(允许多个同键分组)
实现按相邻属性值分组(保留原始顺序,允许重复键分组)
这个场景我太熟悉了!普通的GroupBy确实会把所有相同键的元素一股脑归到同一个组里,完全无视它们在原序列中的位置,肯定满足不了你的需求——你要的是按相邻相同属性值分组(业内常叫"run grouping"),简单说就是:只要当前元素的属性值和前一个不一样,就新建一个分组,相同属性值的连续元素归为一组,非连续的相同属性值会被分成独立的组,严格保留原始顺序。
因为LINQ原生没有提供这个方法,最佳实践是自定义一个符合LINQ风格的扩展方法,既支持延迟执行,又能高效处理序列。
自定义扩展方法实现
下面是完整的实现代码,包含一个通用的GroupAdjacentBy扩展方法,以及辅助的Grouping类(用来实现IGrouping接口,让分组结果和原生GroupBy的输出格式一致):
using System; using System.Collections; using System.Collections.Generic; using System.Linq; public static class EnumerableExtensions { public static IEnumerable<IGrouping<TKey, TElement>> GroupAdjacentBy<TElement, TKey>( this IEnumerable<TElement> source, Func<TElement, TKey> keySelector, IEqualityComparer<TKey> comparer = null) { if (source == null) throw new ArgumentNullException(nameof(source)); if (keySelector == null) throw new ArgumentNullException(nameof(keySelector)); // 使用传入的比较器,默认用EqualityComparer<TKey>.Default comparer = comparer ?? EqualityComparer<TKey>.Default; using var enumerator = source.GetEnumerator(); // 空序列直接返回 if (!enumerator.MoveNext()) yield break; TKey currentKey = keySelector(enumerator.Current); List<TElement> currentGroup = new() { enumerator.Current }; while (enumerator.MoveNext()) { TElement item = enumerator.Current; TKey key = keySelector(item); if (comparer.Equals(currentKey, key)) { // 属性值相同,加入当前分组 currentGroup.Add(item); } else { // 属性值不同,返回当前分组,新建分组 yield return new Grouping<TKey, TElement>(currentKey, currentGroup); currentKey = key; currentGroup = new List<TElement> { item }; } } // 别忘了返回最后一个分组 yield return new Grouping<TKey, TElement>(currentKey, currentGroup); } } // 自定义Grouping类,实现IGrouping接口 public class Grouping<TKey, TElement> : IGrouping<TKey, TElement> { public TKey Key { get; } private readonly IEnumerable<TElement> _elements; public Grouping(TKey key, IEnumerable<TElement> elements) { Key = key; _elements = elements; } public IEnumerator<TElement> GetEnumerator() => _elements.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); }
使用示例
假设我们有这样的实体类和测试序列:
public class Product { public string Category { get; set; } public string Name { get; set; } } // 测试序列:属性值顺序是 A,B,C,B,A(对应5个独立分组) var products = new List<Product> { new() { Category = "A", Name = "Laptop" }, new() { Category = "B", Name = "Desk" }, new() { Category = "C", Name = "Mouse" }, new() { Category = "B", Name = "Chair" }, new() { Category = "A", Name = "Tablet" } };
调用扩展方法并输出结果:
var groupedProducts = products.GroupAdjacentBy(p => p.Category); foreach (var group in groupedProducts) { Console.WriteLine($"分组键: {group.Key}"); foreach (var product in group) { Console.WriteLine($" - {product.Name}"); } }
输出结果:
分组键: A - Laptop 分组键: B - Desk 分组键: C - Mouse 分组键: B - Chair 分组键: A - Tablet
可以看到,相同键的非连续元素被分成了独立分组,完全保留了原始序列的顺序,完美符合你的需求。
替代方案:用Aggregate实现
如果不想写扩展方法,也可以用Aggregate一次性聚合出分组,但这种方式会把整个序列加载到内存中,不适合处理大数据量:
var grouped = products.Aggregate(new List<List<Product>>(), (groups, product) => { if (groups.Count == 0 || groups.Last().First().Category != product.Category) { groups.Add(new List<Product>()); } groups.Last().Add(product); return groups; }) .Select(g => new Grouping<string, Product>(g.First().Category, g));
总结
自定义GroupAdjacentBy扩展方法是最佳选择:
- 符合LINQ的延迟执行特性,内存效率更高
- 通用型强,支持任意元素类型和键类型
- 可以自定义键的比较逻辑(通过传入
IEqualityComparer<TKey>)
内容的提问来源于stack exchange,提问作者Marco Veglio
相关产品推荐
相关产品推荐

