使用条件将一次性使用的大型 IEnumerable<T> 分成两半

Split an single-use large IEnumerable<T> in half using a condition

假设我们有一个 Foo class:

public class Foo
{
    public DateTime Timestamp { get; set; }
    public double Value { get; set; }

    // some other properties

    public static Foo CreateFromXml(Stream str)
    {
        Foo f = new Foo();

        // do the parsing

        return f;
    }

    public static IEnumerable<Foo> GetAllTheFoos(DirectoryInfo dir)
    {
        foreach(FileInfo fi in dir.EnumerateFiles("foo*.xml", SearchOption.TopDirectoryOnly))
        {
            using(FileStream fs = fi.OpenRead())
                yield return Foo.CreateFromXML(fs);
        }
    }
}

为了让您了解情况,我可以说这些文件中的数据已经记录了大约 2 年,频率通常是每分钟几个 Foo。

现在:我们有一个名为 TimeSpan TrainingPeriod 的参数,例如大约 15 天。我想要完成的是调用:

var allTheData = GetAllTheFoos(myDirectory);

并获得其中的 IEnumerable<Foo> TrainingSet, TestSet,其中 TrainingSet 包括记录的前 15 天的 Foos,以及其余所有天数的 TestSet。然后,在 TrainingSet 之外,我们想要计算一些常量内存数据(如平均值 Value、一些线性回归等),然后使用计算值开始使用 TestSet。换句话说,我的代码在语义上 应该等同于:

TimeSpan TrainingPeriod = new TimeSpan(15, 0, 0); // hope it says 15 days

var allTheData = GetAllTheFoos(myDirectory);
List<Foo> allTheDataList = allTheData.ToList();

var threshold = allTheDataList[0].Timestamp + TrainingPeriod;

List<Foo> TrainingSet = allTheDataList.Where(foo => foo.Timestamp < threshold).ToList();
List<Foo> TestSet = allTheDataList.Where(foo => foo.Timestamp >= threshold).ToList();

顺便说一下,XML 文件命名约定向我保证,Foos 将按时间顺序返回。 当然,我不想将它全部存储在内存中,每次调用 .ToList() 时都会发生这种情况。所以我想出了另一个解决方案:

TimeSpan TrainingPeriod = new TimeSpan(15, 0, 0);

var allTheData = GetAllTheFoos(myDirectory);

var threshold = allTheDataList.First().Timestamp + TrainingPeriod; // a minor issue

var grouped = from foo in allTheData
              group foo by foo.Timestamp < Training;

var TrainingSet = grouped.First(g => g.Key);
var TestSet = grouped.First(g => !g.Key); // the major one

但是,关于那段代码有一个小问题和一个大问题。次要的是第一个文件至少被读取了两次——实际上并不重要。但看起来 TrainingSet 和 TestSet 独立访问目录,读取每个文件两次并且 select 仅读取那些持有特定时间戳约束的文件。我对此并不太困惑 - 事实上,如果它有效,我会感到困惑并且不得不再次重新考虑 LINQ。但这会引发文件访问问题,每个文件都会被解析两次,这完全是在浪费 CPU 时间。

所以我的问题是:我可以仅使用简单的 LINQ/C# 工具来实现这种效果吗?我想我可以用一种很好的蛮力方式来做到这一点,覆盖一些 GetEnumerator()MoveNext() 方法等等 - 请不要打扰它,我完全可以自己处理.

但是,如果对此有一些优雅、简短和甜蜜的解决方案,我们将不胜感激。

谢谢!

另一个编辑:

我最终想出的代码如下:

public static void Handle(DirectoryInfo dir)
{
    var allTheData = Foo.GetAllTheFoos(dir);

    var it = allTheData.GetEnumerator();

    it.MoveNext();

    TimeSpan trainingRange = new TimeSpan(15, 0, 0, 0);

    DateTime threshold = it.Current.Timestamp + trainingRange;

    double sum = 0.0;
    int count = 0;

    while(it.Current.Timestamp <= threshold)
    {
        sum += it.Current.Value;
        count++;

        it.MoveNext();
    }

    double avg = sum / (double)count;

    // now I can continue on with the 'it' IEnumerator
}

当然仍然存在一些小问题,即 MoveNext() 的输出非常重要(它已经结束 IEnumerable 了吗?),但我希望总体思路很清楚。 BUT 在实际代码中,我计算的不仅仅是平均值,还有不同类型的回归等。所以我想以某种方式提取第一部分,将其作为 IEnumerable 传递给class 来自我的

public abstract class AbstractAverageCounter
{
    public abstract void Accept(IEnumerable<Foo> theData);
    public AverageCounterResult Result { get; protected set; }
}

分离训练数据的提取和处理的责任。再加上在我得到 IEnumerator<Foo> 之前描述的过程之后,但我认为 IEnumerable<Foo> 更适合将其传递给我的 TheRestOfTheDataHandler 实例。

您可以尝试在从初始 ienumerable 获得的 ienumerator 上实施有状态迭代器模式。

IEnumerable<T> StatefulTake(IEnumerator<T> source, Func<bool> getDone, Action setDone);

此方法仅检查完成,调用 MoveNext,生成 Current 并在 movenext 返回 false 时更新完成。

然后您将您的集合拆分为对该方法的后续调用,并使用以下方法对其进行部分枚举,例如: 暂时 任何 第一的 ... 然后你可以在上面做任何操作,但是每一个都必须枚举到最后。

var source = GetThemAll();
using (var e = source.GetEnumerator()){
 bool done=!source.MoveNext();
 foreach(var i in StatefulTake(e, ()=>done,()=>done=true).TakeWhile(i=>i.Time<...)){
  //...
 }

 var theRestAverage = StatefulTake(e,()=>done,()=>done=true).Avg(i=>i.Score);
 //...
}

这是我在我的异步工具包中经常使用的模式。

更新:修复了StatefulTake方法的签名,它不能使用ref参数。对 MoveNext 的初始调用也是必要的。三种done变量引用和方法本身应该封装在上下文class.