基于谓词拆分LINQ查询

15
我想要一个方法,它可以按照谓词将一个IEnumerable拆分,并根据它们相对于谓词的索引将项分组在一起。例如,它可以将一个List根据满足x => MyRegex.Match(x).Success的项拆分,这些项之间被分组在一起。
其签名可能看起来像:
public static IEnumerable<IEnumerable<TSource>> Split<TSource>(
    this IEnumerable<TSource> source,
    Func<TSource, bool> predicate,
    int bool count
)

可能还有一个输出元素包含所有分隔符的额外元素。

是否有比使用 foreach 循环更高效、更紧凑的实现方式?我感觉应该可以使用 LINQ 方法实现,但我无法确定。

示例:

string[] arr = {"One", "Two", "Three", "Nine", "Four", "Seven", "Five"};
arr.Split(x => x.EndsWith("e"));

以下任一种方式都可以:

IEnumerable<string> {{}, {"Two"}, {}, {"Four", "Seven"}, {}}
IEnumerable<string> {{"Two"}, {"Four", "Seven"}}

可选的元素用于存储匹配项,将是{"One", "Three", "Nine", "Five"}


10
除了“group by”之外,你是指其他什么吗? - lc.
1
@Arithmomaniac,你是指像Skip()Take()这样的东西吗? - Frédéric Hamidi
@Tyrsius,看起来他想根据谓词的结果对项目进行分组,但进一步的评论说“基于位置”,所以这可能比那更复杂。 - Frédéric Hamidi
抱歉,我还在学习如何提供好的示例。 - Arithmomaniac
1
你能写出符合你意思的foreach循环吗? - neontapir
显示剩余5条评论
5个回答

45

如果您想避免使用扩展方法,您可以始终使用:

var arr = new[] {"One", "Two", "Three", "Nine", "Four", "Seven", "Five"};

var result = arr.ToLookup(x => x.EndsWith("e"));

// result[true]  == One Three Nine Five
// result[false] == Two Four Seven

这是一个巧妙的技巧,但它并不能保持真正的项由拆分组合。 - Arithmomaniac
谢谢,这对我解决类似的问题很有帮助。 - Jonathan Wilson
1
这是一个不错的解决方案。此外,我们可以用积极的词汇来命名变量。在安迪的例子中,将变量命名为 itemsEndWithE,因此 itemsEndWithE[false] 表示项目不以 E 结尾。我们不需要额外的变量来表示这一点。 - Gqqnbig
1
这是一个不错的解决方案,尽管它是针对一个不同的问题。这个标题听起来像是另一个问题,而不是实际被问到的问题。 - Yair Halberstadt
1
因为LookUp允许空输入和空输出,所以额外加分。 - aloisdg
显示剩余2条评论

7
你应该通过扩展方法来实现这个(本方法假设你忽略了分区项):
/// <summary>Splits an enumeration based on a predicate.</summary>
/// <remarks>
/// This method drops partitioning elements.
/// </remarks>
public static IEnumerable<IEnumerable<TSource>> Split<TSource>(
    this IEnumerable<TSource> source,
    Func<TSource, bool> partitionBy,
    bool removeEmptyEntries = false,
    int count = -1)
{
    int yielded = 0;
    var items = new List<TSource>();
    foreach (var item in source)
    {
        if (!partitionBy(item))
            items.Add(item);
        else if (!removeEmptyEntries || items.Count > 0)
        {
            yield return items.ToArray();
            items.Clear();

            if (count > 0 && ++yielded == count) yield break;
        }
    }

    if (items.Count > 0) yield return items.ToArray();
}

1
@FrédéricHamidi:是的,他的例子包括中间的空元素。 - user7116
2
@Arithmomaniac:你可以轻松修改这个算法来处理那种情况。但我选择模仿String.Split的方式。入乡随俗! - user7116
这种方法和Servy的方法的问题在于它使用一个内部的List<>来收集每组项目。后果是您无法惰性地评估内部组。如果要从序列中取出第一个值,它仍将评估整个第一个内部组。如果序列是“one”的无限重复,则永远无法从Split中获取任何值,并且列表填充时会收到OutOfMemoryException。我尝试重写此方法以使用惰性评估,但未成功,尽管我可以使用Observable使其工作。 - Niall Connaughton
@NiallConnaughton:我不确定你能否使用标准的延迟评估方法重写它。我想在一个将输入集分区的方案中,你不会询问第一个(或第N个)分区的第一个项目的问题。在这些情况下,你应该使用TakeWhile - user7116
这取决于你想要多通用,我想。如果你的序列生成成本高,或者生成具有副作用,则不要枚举更多。例如,如果它是质数序列,根据你的谓词,你可能会评估数百个消费者不关心的质数,只是为了获得它需要的值。显然,这里的解决方案有效,但我更喜欢找到不显示状态的可观察迹象的解决方案。在这种情况下很棘手。 - Niall Connaughton
显示剩余3条评论

3
public static IEnumerable<IEnumerable<TSource>> Split<TSource>(
    this IEnumerable<TSource> source,
    Func<TSource, bool> predicate)
{
    List<TSource> group = new List<TSource>();
    foreach (TSource item in source)
    {
        if (predicate(item))
        {
            yield return group.AsEnumerable();
            group = new List<TSource>();
        }
        else
        {
            group.Add(item);
        }
    }
    yield return group.AsEnumerable();
}

这里调用AsEnumerable对列表有什么意义吗? - Drew Noakes

2
public static IEnumerable<IEnumerable<TSource>> Partition<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
    yield return source.Where(predicate);
    yield return source.Where(x => !predicate(x));
}

例子:

var list = new List<int> { 1, 2, 3, 4, 5 };
var parts = list.Partition(x => x % 2 == 0);
var even = parts.ElementAt(0); // contains 2, 4
var odd = parts.ElementAt(1); // contains 1, 3, 5

4
这种解决方案对性能不是很理想,因为你需要两次遍历列表。 - James Esh

1
我会使用提供的键选择器对源集合进行分区。这样,您还可以根据简单属性切割复杂对象。
public static class LinqExtension
{
    public static IEnumerable<IEnumerable<TSource>> Slice<TSource, TKey>(
        this IEnumerable<TSource> source,
        Func<TSource, TKey> selector,
        Func<TKey, TKey, bool> partition)
    {
        if (source == null) throw new ArgumentNullException(nameof(source));
        if (selector == null) throw new ArgumentNullException(nameof(selector));
        if (partition == null) throw new ArgumentNullException(nameof(partition));

        var seed = new List<List<TSource>> { new List<TSource>() };

        return source.Aggregate(seed, (slices, current) => 
        {
            var slice = slices.Last();
            if (slice.Any())
            {
                var previous = slice.Last();
                if (partition(selector(previous), selector(current)))
                {
                    slice = new List<TSource>();
                    slices.Add(slice);
                }
            }
            slice.Add(current);
            return slices;

        }).Select(x => x.AsReadOnly());
    }
}

一个微不足道的例子:

// slice when the difference between two adjacent elements is bigger than 5
var input = new[] { 1, 2, 3, 10, 11, 20 };
var output = input.Slice(i => i, (x, y) => y - x > 5);

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