Linq/C#:如何根据列表项信息将列表拆分为可变长度的块

本文关键字:列表 拆分 信息 何根 Linq | 更新日期: 2023-09-27 17:57:55

我正试图根据某些Type信息将Linq中类型为Record的列表拆分为子列表。每组记录之前总是有一条类型为"a"的记录,之后总是有一个类型为"b"的记录。我有一个课程Record:

class Record
{
    public string Type { get; set; }
    public string SomeOtherInformation { get; set; }
}

以下是示例列表(List<Record> records):

Type    SomeOtherInformation
a       ......
x       ......
x       ......
b       ......
a       ......
b       ......
a       ......
x       ......
x       ......
x       ......
x       ......
x       ......
b       ......

所需输出为(List<List<Record>> lists):

List #1:        List #2:        List #3:
a       ......  a       ......  a       ......
x       ......  b       ......  x       ......
x       ......                  x       ......
b       ......                  x       ......
                                x       ......
                                x       ......
                                b       ......

我目前正在使用for循环浏览这个列表,每当类型为"a"时创建一个新列表,当项目的类型为"b"时将其添加到子列表中。我想知道林克是否有更好的方法。林克能做到这一点吗?如果可以,怎么做?

Linq/C#:如何根据列表项信息将列表拆分为可变长度的块

据我所知,用普通的LINQ不能干净地做到这一点。LINQ中的流媒体运营商依赖于您能够根据仅该项目,以及可能在原始源中的索引,对项目做出决定(例如,是否过滤它,如何投影它,如何对它进行分组)。在您的情况下,您确实需要更多的信息——您需要知道您已经看到了多少b项目。

可以这样做:

int bs = 0;
var groups = records.GroupBy(item => item.Type == 'b' ? bs++ : bs,
                             (key, group) => group.ToList())
                    .ToList();

然而,这取决于分组投影中b++副作用(以跟踪我们已经看到的b项目的数量)-这肯定不是惯用的LINQ,我不推荐它。

我会为此使用一个扩展方法:

public static IEnumerable<IEnumerable<TSource>> SplitItems<TSource>(
        this IEnumerable<TSource> source,
        Func<TSource, bool> startItem, 
        Func<TSource, bool> endItem)
{
     var tempList = new List<TSource>();
     int counter = 0;
     foreach (var item in source)
     {
         if (startItem(item) || endItem(item)) counter++;
         tempList.Add(item);
         if (counter%2 == 0)
         {
            yield return tempList;
            tempList = new List<TSource>();
         }
      }
}

以下是用法:

var result = list.SplitItems(x => x.Type == "a", x => x.Type == "b").ToList();

这将返回一个包含3项目的List<IEnumerable<Record>>。当然,该方法假设至少在开头有一个开始项,在结尾有一个结束项。您可能需要添加一些检查,并根据您的要求进行改进。

绝对不是纯LINQ,但我可以想象在循环中使用TakeWhile来实现这一点:

List<Record> data;
List<List<Record>> result = new List<List<Record>>();
IEnumerable<Record> workingData = data;
while (workingData.Count() > 0)
{
    IEnumerable<Record> subList = workingData.Take(1).Concat(workingData.Skip(1).TakeWhile(c => c.Type != 'a'));
    result.Add(subList.ToList());
    workingData = workingData.Except(subList);
}

为了解释,我们在序列的开头得到一个我们知道的"a",然后跳过它,直到我们遇到另一个"a"。这构成了其中一个子记录,所以我们将其添加到结果中。然后,我们从"工作"集中删除这个subList,并再次枚举,直到元素用完为止。

我不确定这会比你现有的解决方案更好,但希望它能有所帮助!

这实际上是有效的(在VS2013,.NET4.5.1上测试),使用workingData而不是循环中的数据(我的一个拼写错误,上面已经修复)。Except将使用默认的比较器来比较对象,因为我们没有重写.Equals,它将比较引用(实际上是指针)。因此,重复数据不是问题。如果重写了.Equals,则需要确保每条记录都是唯一的。

如果有人想验证这一点,这里是我的测试程序(只需在Console.ReadKey上放一个断点,你就会看到结果有正确的数据):

class Program
{
    static void Main(string[] args)
    {
        List<Record> testData = new List<Record>()
        {
            new Record() { Type = 'a', Data="Data" },
            new Record() { Type = 'x', Data="Data" },
            new Record() { Type = 'b', Data="Data" },
            new Record() { Type = 'a', Data="Data" },
            new Record() { Type = 'x', Data="Data" },
            new Record() { Type = 'x', Data="Data" },
            new Record() { Type = 'x', Data="Data" },
            new Record() { Type = 'x', Data="Data" },
            new Record() { Type = 'x', Data="Data" },
            new Record() { Type = 'x', Data="Data" },
            new Record() { Type = 'b', Data="Data" },
            new Record() { Type = 'a', Data="Data" },
            new Record() { Type = 'x', Data="Data" },
            new Record() { Type = 'x', Data="Data" },
            new Record() { Type = 'x', Data="Data" },
            new Record() { Type = 'b', Data="Data" }
        };
        List<List<Record>> result = new List<List<Record>>();
        IEnumerable<Record> workingData = testData;
        while (workingData.Count() > 0)
        {
            IEnumerable<Record> subList = workingData.Take(1).Concat(workingData.Skip(1).TakeWhile(c => c.Type != 'a'));
            result.Add(subList.ToList());
            workingData = workingData.Except(subList);
        }
        Console.ReadKey();
    }
}
class Record
{
    public char Type;
    public String Data;
}

如前所述,LINQ不能很好地处理这种情况,因为您只能根据当前项目而不是以前看到的项目来做出决策。您需要维护某种状态来跟踪分组。依赖副作用

编写自己的扩展方法将是更好的选择。您可以保持状态,并使其完全自包含(与现有的操作符(如GroupBy()和其他操作符)非常相似)。这是我的一个实现,它可以选择性地包括起始项和结束项中不包含的项。

public static IEnumerable<IImmutableList<TSource>> GroupByDelimited<TSource>(
    this IEnumerable<TSource> source,
    Func<TSource, bool> startDelimiter,
    Func<TSource, bool> endDelimiter,
    bool includeUndelimited = false)
{
    var delimited = default(ImmutableList<TSource>.Builder);
    var undelimited = default(ImmutableList<TSource>.Builder);
    foreach (var item in source)
    {
        if (delimited == null)
        {
            if (startDelimiter(item))
            {
                if (includeUndelimited && undelimited != null)
                {
                    yield return undelimited.ToImmutable();
                    undelimited = null;
                }
                delimited = ImmutableList.CreateBuilder<TSource>();
            }
            else if (includeUndelimited)
            {
                if (undelimited == null)
                {
                    undelimited = ImmutableList.CreateBuilder<TSource>();
                }
                undelimited.Add(item);
            }
        }
        if (delimited != null)
        {
            delimited.Add(item);
            if (endDelimiter(item))
            {
                yield return delimited.ToImmutable();
                delimited = null;
            }
        }
    }
}

但是,如果真的愿意,您仍然可以使用LINQ运算符(Aggregate())来完成此操作,但这不是一个真正的LINQ解决方案。它将再次看起来像一个自包含的foreach循环。

var result = records.Aggregate(
    Tuple.Create(default(List<Record>), new List<List<Record>>()),
    (acc, record) =>
    {
        var grouping = acc.Item1;
        var result = acc.Item2;
        if (grouping == null && record.Type == "a")
        {
            grouping = new List<Record>();
        }
        if (grouping != null)
        {
            grouping.Add(record);
            if (record.Type == "b")
            {
                result.Add(grouping);
                grouping = null;
            }
        }
        return Tuple.Create(grouping, result);
    },
    acc => acc.Item2
);