TakeWhile,但我也想要输入序列的其余部分



我想要一些有效执行与TakeWhile相同但返回两个序列的东西:

  1. TakeWhile的结果
  2. 删除了输入序列的其余部分,其中 1.

我知道我可以做这样的事情:

var a = input.TakeWhile(...);
var b = input.Skip(a.Count);

但这似乎不是最佳的,具体取决于容器类型。我是否错过了一些在单个操作中执行此操作的巧妙方法?

我的最终目标是遍历大型集合,而不是预先存储它:

while(data.Count() > 0)
{
var y = data.First().Year;
var year = data.TakeWhile(c => c.Year == y);
data = data.Skip(year.Count());
Console.WriteLine($"{year.Count()} items in {y}");
}

您可以使用 ToLookup 将源拆分为两个结果。

var source = new[] { 1, 3, 5, 2, 4, 6, 7, 8, 9 };
Func<int, bool> criteria = x => x % 2 == 1;
bool stillGood = true;
Func<int, bool> takeWhileCriteria = x =>
stillGood = stillGood && criteria(x);
var result = source.ToLookup(takeWhileCriteria);
var matches = result[true];
var nonMatches = result[false];

在一次迭代和流式处理中拆分序列的最简单方法是返回每个项目的元组和一个bool,无论它是否"在"。

public static IEnumerable<(T Entity, bool IsIn)> MarkWhile<T>(this IEnumerable<T> sequence, 
Func<T,bool> predicate)
{
var isIn = true;
using var etor = sequence.GetEnumerator();
while (etor.MoveNext())
{
var current = etor.Current;
isIn &= predicate(current);
yield return (current, isIn);
}
}

这允许您在不耗尽大型集合的情况下迭代它,并确定条件何时"翻转"。但是您需要一个foreach循环才能一次性完成此操作。

可以创建一个仅耗尽序列的"in"部分甚至返回其计数的方法(我们可以在返回元组时执行任何操作)并流式传输序列的尾部,但我会用一个简单的foreach来解决。这没什么不对。此外,在某些情况下,所有商品都满足条件,而您仍然只想退回有限数量的商品。

您可以创建所需的内容,但仅限于非常有限的情况下:

public static class IEnumerableExt {
public static IEnumerable<T> ToIEnumerable<T>(this IEnumerator<T> e) {
while (e.MoveNext())
yield return e.Current;
}
public static (IEnumerable<T> first, IEnumerable<T> rest) FirstRest<T>(this IEnumerable<T> src, Func<T,bool> InFirstFn) {
var e = src.GetEnumerator();
var first = new List<T>();
while (e.MoveNext() && InFirstFn(e.Current))
first.Add(e.Current);
return (first, e.ToIEnumerable());
}
}

请注意,这必须迭代并缓冲first才能返回(如果您尝试在first之前枚举rest怎么办?),并且您不能在rest上调用Reset并期望任何合理的内容。解决这些问题将涉及更多代码。

我可以在远处隐约看到某种类型的扩展 LINQ,您可以在其中传递Actions 和Funcs,并执行类似延续(IEnumerable的其余部分)之类的操作来处理,但我不确定这是否值得。像这样:

public static IEnumerable<T> DoWhile<T>(this IEnumerable<T> src, Func<T,bool> whileFn, Action<T> doFn) {
var e = src.GetEnumerator();
while (e.MoveNext() && whileFn(e.Current))
doFn(e.Current);

return e.ToIEnumerable();
}

虽然你可以这样使用:

while (data.Any()) {
var y = data.First().Year;
var ct = 0;
data = data.DoWhile(d => d.Year == y, d => ++ct);

Console.WriteLine($"{ct} items in {y}");
}

最佳答案是停止使用IEnumerable<T>自动枚举并手动枚举:

for (var e = data.GetEnumerator(); e.MoveNext();) {
var y = e.Current.Year;
var ct = 0;
while (e.Current.Year == y)
++ct;
Console.WriteLine($"{ct} items in {y}");
}

执行手动枚举后,可以处理大多数情况,而不会降低缓冲效率,或根据特定需求委派调用。

PS:请注意,针对0测试data.Count()效率非常低,您应该始终使用data.Any()。根据datadata.Count()可能永远不会回来,或者可能非常昂贵,但即使data.Any()也可能失去data.First()

PPS:更高效的ToIEnumerable版本将返回一个自定义类,该类仅将IEnumerator返回给GetEnumerator,但会包含所有警告,甚至可能更多。示例ToEnumerable创建while循环的菊花链。

您可以使用LINQ 的 Except扩展,因为 linq 运算符总是创建新集合,而不保留第一个集合:

var list = Enumerable.Range(1,10);
var lowerFive = list.TakeWhile(x => x < 5); // 1,2,3,4
var rest = list.Except(lowerFive); // 5,6,7,8,9,10

你可以在 IEnumerable 上编写一个泛型扩展,它返回一个包含两个列表的元组:

public static class Extensions
{
public static (IEnumerable<T> takeWhilePart, IEnumerable<T> rest) TakeWhileAndTheRest<T>(this IEnumerable<T> origin, Func<T, bool> predicate)
{
var takeWhile = origin.TakeWhile(predicate);
var rest = origin.Except(takeWhile);
return (takeWhile, rest);
}
}

并像这样使用它:

void Main()
{
var list = Enumerable.Range(1,10);
var collectionTuple = list.TakeWhileAndTheRest(x => x < 5);

collectionTuple.takeWhilePart.Dump(); // 1,2,3,4
collectionTuple.rest.Dump(); // 5,6,7,8,9,10
}

相关内容

最新更新