如何使用LINQ将字符串列表按特定条件拆分为其他列表?

3

我有一个包含{a,b,c,at,h,c,bt}的列表,我想把它分成List<List<string>>{{a,b,c},{at,h,c},{bt}};如果特定字符串包含"t",我需要在那一行中断,如何在linq中实现这一点?


我不认为这种类型的事情非常适合于LINQ,但也许有人可以想出什么办法。 - leppie
4个回答

4

嗯,有一种非常糟糕的方法:

int tCounter = 0;
var groups = sequence.GroupBy(x => x.Contains("t") ? ++tCounter : tCounter)
                     .Select(group => group.ToList())
                     .ToList();

或者等价地(但不调用Select函数):
int tCounter = 0;
var groups = sequence.GroupBy(x => x.Contains("t") ? ++tCounter : tCounter,
                              (count, group) => group.ToList())
                     .ToList();

这依赖于GroupBy子句中的副作用,这是一个非常糟糕的想法。LINQ是围绕函数式理念设计的,查询不应该有副作用。你应该在使用查询的代码中放置副作用,而不是在查询本身中放置副作用。这样做可以工作,但我不建议。

这里有一个简短但完整的演示,只是为了证明它确实可以工作:

using System;
using System.Collections.Generic;
using System.Linq;

public class Test
{
    static void Main(string[] args)
    {
        var input = new List<string>{"a","b","c","at","h","c","bt"};

        int tCounter = 0;
        var groups = input.GroupBy(x => x.Contains("t") ? ++tCounter : tCounter)
                          .Select(group => group.ToList())
                          .ToList();
        foreach (var list in groups)
        {
            Console.WriteLine(string.Join(", ", list));
        }
    }
}

输出:

a, b, c
at, h, c
bt

我们真正需要的是一个“扫描”(又名foldl,我想 - 不确定)运算符 - 类似于Aggregate,但提供一个运行聚合。然后,扫描可以跟踪当前T的数量以及当前值,而GroupBy可以在此基础上工作。
编写这样的运算符并不难,而且如果我没记错的话,Reactive Extensions System.Interactive程序集已经包含了一个。您可能希望使用该程序集,而不是使用我的可怕的hack。此时,您实际上可以相当优雅地在LINQ中编写它。

这是一个情况的例子,可以通过给你投反对票来强调给问题提问者他们所采取的方法是错误的吗? - Gabe
1
@Gabe:嗯,我不会抱怨,但我确实提到这个解决方案是可怕的,所以我不认为它是在倡导不良实践。事实上,它说这是一个坏主意以及为什么。我并不认为在这里使用LINQ是一个真正的坏主意 - 我们只需要一个标准库中目前没有的运算符。 - Jon Skeet
一个小问题,但我相当确定foldl只是LINQ的标准Aggregate方法的等价物。 - LukeH

2

您需要的正是内置扩展方法Aggregate

var source = new List<string> { "a", "b", "c", "at", "h", "c", "bt" };
var result = source.Aggregate(new List<List<string>>(), (list, s) =>
    {
        if (list.Count == 0 || s.Contains('t')) list.Add(new List<string>());
        list.Last().Add(s);
        return list;
    });

resultList<List<string>>


1

我认为使用内置的Linq方法无法完成(实际上,可以...请参见其他答案),但是您可以轻松地创建自己的扩展方法来实现此目的:

public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> source, Func<T, bool> isSeparator)
{
    List<T> list = new List<T>();
    foreach(T item in source)
    {
        if (isSeparator(item))
        {
            if (list.Count > 0)
            {
                yield return list;
                list = new List<T>();
            }
        }
        list.Add(item);
    }

    if (list.Count > 0)
    {
        yield return list;
    }
}

然后按照以下方式使用:

var list = new[] { "a", "b", "c", "at", "h", "c", "bt" };
var result = list.Split(s => s.Contains("t"));

0

这个问题对我来说并不是显然需要使用LINQ。如果你只是想练习一下使用LINQ,那就另当别论了。但是,以下是我如何解决它的方法(使用普通的循环):

        List<List<string>> list = new List<List<string>>();
        List<string> sublist = new List<string>();
        foreach (string element in originalList)
        {
            if (element.Contains("t"))
            {
                list.Add(sublist);
                sublist = new List<string>();
            }
            sublist.Add(element);
        }
        list.Add(sublist);

别误会,我比任何人都更喜欢使用LINQ。 :)


1
@Sapph:我对你最后的说法提出异议。我见过并且也犯过一些相当可怕的 LINQ 滥用行为... - Jon Skeet
@Jon Skeet - 我必须承认,当我意识到你的解决方案正在做什么时,我的嘴巴张开了。 :D - Sapph
你的解决方案是唯一有时返回空子列表的方案。 - Gabe
@The:我实际上没有测试任何答案;我只是看了一下它们,以理解它们的工作原理。 - Gabe
@Gabe:在回答时我考虑过这一点,但问题并没有指定所需的行为(例如,string.split提供了包括或不包括空子字符串的选项)。 - Sapph
Sapph:很好,你考虑到了这一点。我只是提到它,以防有些读者不够敏锐,无法识别其中的区别。 - Gabe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接