嵌套产生返回 IEnumerable> 并带有惰性求值

2024-01-13

我写了一个LINQ扩展方法SplitBetween类似于String.Split.

> new List<int>(){3,4,2,21,3,2,17,16,1}
> .SplitBetween(x=>x>=10)

[3,4,2], [3,2], [], [1]

Source:

// partition sequence into sequence of contiguous subsequences
// behaves like String.Split
public static IEnumerable<IEnumerable<T>> SplitBetween<T>(this IEnumerable<T> source, 
                                                          Func<T, bool> separatorSelector, 
                                                          bool includeSeparator = false)
{
    var l = new List<T>();
    foreach (var x in source)
    {
        if (separatorSelector(x))
        {
            if (includeSeparator)
            {
                l.Add(x);
            }
            yield return l;
            l = new List<T>();
        }
        else
        {
            l.Add(x);
        }
    }
    yield return l;
}

本着 LINQ 的精神,我认为这个方法应该进行惰性评估。然而,我的实现对外部 IEnumerable 进行了惰性评估,但是不超过内部 IEnumerable。我怎样才能解决这个问题?

外部行为如何是懒惰的演示。认为ThrowingEnumerable<int> is an IEnumerable<int>当任何人试图迭代它时它就会爆炸(参见 Skeet 的 Edulinq)。

(new List<int>(){1,2,3,10,1})
.Concat(Extensions.ThrowingEnumerable<int>())
.SplitBetween(x=>x>=10)
.First().ToList();

[1,2,3]

但内心的行为不懒

(new List<int>(){1,2,3,10,1})
.Concat(Extensions.ThrowingEnumerable<int>())
.SplitBetween(x=>x>=10)
.ElementAt(2).First();

BOOM

我们期望这里有 1。


Edit:你的方法没有任何问题,除了当你枚举它时,抛出可枚举确实会“繁荣”。这就是它的目的。它没有一个合适的GetEnumerator对其进行定义。所以你的代码没有表现出真正的问题。在第一种情况下,通过这样做First,你只是枚举直到第一个结果集(只是{ 1, 2, 3 } )被获得并且没有枚举抛出可枚举(这意味着Concat没有被执行)。但在第二个示例中,您要求的元素位于2分割后,这意味着它也会枚举投掷可枚举,并且会“繁荣”。这里的关键是要理解ElementAt 枚举集合直到索引要求并且本质上不是懒惰的(它不可能是)。

我不确定完全懒惰是否是正确的选择。问题在于,惰性分割为外部序列和内部序列的整个过程在一个枚举器上运行,这可能会根据枚举器状态产生不同的结果。例如,您仅枚举外部序列,内部序列将不再是您所期望的。或者,如果只枚举一半的外部序列和一个内部序列,那么其他内部序列的状态会是什么?你的方法是最好的。

下面的方法是懒惰的(仍然会繁荣,因为这是有保证的),因为它不使用中间具体实现,但可能比原来的方法慢,因为它多次遍历列表:

public static IEnumerable<IEnumerable<T>> SplitBy<T>(this IEnumerable<T> source, 
                                                     Func<T, bool> separatorPredicate, 
                                                     bool includeEmptyEntries = false,
                                                     bool includeSeparators = false)
{
    int prevIndex = 0;
    int lastIndex = 0;
    var query = source.Select((t, index) => { lastIndex = index; return new { t, index }; })
                      .Where(a => separatorPredicate(a.t));
    foreach (var item in query)
    {
        if (item.index == prevIndex && !includeEmptyEntries)
        {
            prevIndex++;
            continue;
        }

        yield return source.Skip(prevIndex)
                           .Take(item.index - prevIndex + (!includeSeparators ? 0 : 1));
        prevIndex = item.index + 1;
    }

    if (prevIndex <= lastIndex)
        yield return source.Skip(prevIndex);
}

总的来说,你原来的方法是最好的。如果您需要完全懒惰的东西,那么我的以下答案很合适。请注意,它仅适用于以下情况:

foreach (var inners in outer)
    foreach (var item in inners)
    { 
    }

and not

var outer = sequence.Split;
var inner1 = outer.First;
var inner2 = outer.ElementAt; //etc

换句话说,不适合同一内部序列的多次迭代。如果您完全意识到这种危险的构造:


原答案:

这不使用中间具体集合,不ToList在源可枚举上,并且完全是惰性/迭代器式的:

public static IEnumerable<IEnumerable<T>> SplitBy<T>(this IEnumerable<T> source,
                                                     Func<T, bool> separatorPredicate,
                                                     bool includeEmptyEntries = false,
                                                     bool includeSeparator = false)
{
    using (var x = source.GetEnumerator())
        while (x.MoveNext())
            if (!separatorPredicate(x.Current))
                yield return x.YieldTill(separatorPredicate, includeSeparator);
            else if (includeEmptyEntries)
            {
                if (includeSeparator)
                    yield return Enumerable.Repeat(x.Current, 1);
                else
                    yield return Enumerable.Empty<T>();
            }
}

static IEnumerable<T> YieldTill<T>(this IEnumerator<T> x, 
                                   Func<T, bool> separatorPredicate,
                                   bool includeSeparator)
{
    yield return x.Current;

    while (x.MoveNext())
        if (!separatorPredicate(x.Current))
            yield return x.Current;
        else
        {
            if (includeSeparator)
                yield return x.Current;
            yield break;
        }
}

简短、甜蜜、简单。我添加了一个附加标志来表示是否要返回空集(默认情况下它会忽略)。如果没有这个标志,代码会更加简洁。

感谢您提出这个问题,这将在我的扩展方法库中! :)

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

嵌套产生返回 IEnumerable> 并带有惰性求值 的相关文章

随机推荐