You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 07:14:49