nest产生以懒惰评估返回IEnumerable <IEnumerable <T >>

Col*_*nic 13 .net c# linq ienumerable lazy-evaluation

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

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

[3,4,2], [3,2], [], [1]
Run Code Online (Sandbox Code Playgroud)

资源:

// 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;
}
Run Code Online (Sandbox Code Playgroud)

在LINQ的精神下,我认为这种方法应该做懒惰的评估.但是,我的实现对外部IEnumerable进行了懒惰的评估,但没有超过内部的IEnumerable.我怎样才能解决这个问题?

演示外部行为是如何懒惰的.当任何人试图迭代它时,假设ThrowingEnumerable<int>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]
Run Code Online (Sandbox Code Playgroud)

但内心的行为并不是懒惰的

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

BOOM
Run Code Online (Sandbox Code Playgroud)

我们期待在这里1.

dob*_*lak 1

我想这是一个可以满足您要求的解决方案。

问题是您只有一种具有收益的方法,并且您在枚举外部集合IEnumerable手动创建内部集合。第二个问题是,即使在下面的我的代码中,您的“测试”方式也会失败。然而,正如 David B 在他的评论中指出的那样,您必须遍历所有元素才能定义 external 的元素数量IEnumerable。但是您可以推迟内部 s 的创建和填充IEnumerable

public static IEnumerable<IEnumerable<T>> SplitBetween<T>(this IEnumerable<T> source,Func<T,bool> separatorSelector, bool includeSeparators=false)
{
    IList<T> sourceList = source.ToList();
    var indexStart = 0;
    var indexOfLastElement = sourceList.Count - 1;
    for(int i = 0; i <= indexOfLastElement; i++)
        if (separatorSelector(sourceList[i]))
        {
            if(includeSeparators)
                yield return SplitBetweenInner(sourceList, indexStart, i);
            else
                yield return SplitBetweenInner(sourceList, indexStart, i - 1);

            indexStart = i + 1;
        }
        else if(i == indexOfLastElement)
            yield return SplitBetweenInner(sourceList, indexStart, i);        
}

private static IEnumerable<T> SplitBetweenInner<T>(IList<T> source, int startIndex, int endIndex)
{
    //throw new Exception("BOOM");
    for(int i = startIndex; i <= endIndex; i++)
        yield return source[i];
}
Run Code Online (Sandbox Code Playgroud)

请注意,它的行为与您的代码略有不同(当最后一个元素满足分隔符条件时,它不会创建另一个空列表 - 这取决于定义这里正确的内容,但我发现更好,因为行为与如果元素出现在源列表的开头)

如果您测试代码,您将看到内部IEnumerable执行被延迟。

如果抛出异常行未注释:

(new List<int>(){3,4,2,21,3,2,17,16,1}).SplitBetween(x=>x>=10, true).Count();
Run Code Online (Sandbox Code Playgroud)

回报4

(new List<int>(){3,4,2,21,3,2,17,16,1}).SplitBetween(x=>x>=10, true).First().Count();
Run Code Online (Sandbox Code Playgroud)

投掷BOOM

  • 时间复杂度 - O(n^2 + 2n),因为每次调用的 ElementAt 时间复杂度为 O(n/2),而 Count 时间复杂度为 O(n) (2认同)
  • 当源参数不是 IList 且最坏情况的时间复杂度为 O(n^2 + 2n) 时的最坏情况 (2认同)