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

时间:2014-04-11 22:35:49

标签: c# .net linq list

我正在尝试根据某些Record信息将Linq中的Type类型列表拆分为子列表。在每组记录之后总是有一个类型为“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是否有更好的方法。可以用Linq完成,如果是的话,怎么做?

4 个答案:

答案 0 :(得分:4)

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

可以这样做:

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

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

答案 1 :(得分:2)

我会使用扩展方法代替:

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();

这将为您返回List<IEnumerable<Record>>3项。因此,该方法假设开头至少有一个开始项,最后一个项结束。您可能需要添加一些检查并根据您的要求进行改进。

答案 2 :(得分:1)

绝对不是纯粹的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);
}

为了解释,我们得到了&#39; a&#39;我们知道是在我们的序列的开头,然后跳过它,直到我们遇到另一个&#39; a&#39;。这构成了其中一个子记录,因此我们将其添加到结果中。然后我们从&#34; working&#34;中删除这个子列表。设置,并再次枚举,直到我们用完元素。

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

这实际上可行,(在VS 2013,.NET 4.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;
}

答案 3 :(得分:0)

正如已经提到的,这不是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
);