【问题标题】:Split an single-use large IEnumerable<T> in half using a condition使用条件将一次性大型 IEnumerable<T> 分成两半
【发布时间】:2015-04-17 01:21:22
【问题描述】:

假设我们有一个Foo 类:

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);
        }
    }
}

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

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

var allTheData = GetAllTheFoos(myDirectory);

并获取其中的IEnumerable&lt;Foo&gt; TrainingSet, TestSet,其中TrainingSet 由记录前15 天的Foos 和其余所有的TestSet 组成。然后,在TrainingSet 之外,我们要计算一些常量内存数据(如平均值Value,一些线性回归等),然后使用计算值开始使用测试集。换句话说,我的代码应该语义上等同于:

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 独立访问目录,读取每个文件两次并只选择那些持有特定时间戳约束的文件。我对此并不感到困惑——事实上,如果它有效,我会感到困惑,并且不得不再次重新考虑 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 传递给派生自的类我的

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

分离提取训练数据和处理数据的职责。加上在我得到IEnumerator&lt;Foo&gt; 之前描述的过程之后,但我认为IEnumerable&lt;Foo&gt; 最好将它传递给我的TheRestOfTheDataHandler 实例。

【问题讨论】:

  • 如果您没有将它们全部保存在内存中,您希望如何处理它们?您不能期望在两个组之间跳过并且只流式传输一次。你的价值观都是按时间顺序排列的吗?目前还不清楚您要寻找什么样的答案。
  • @JonSkeet 他不能只创建两个自定义 IEnumerable(由单源 IEnumerable 支持)吗?这样他正确划分集合并流一次
  • @joozek:但是,如果您想查看集合 X 中的第一个值,而目前流中的所有内容都在集合 Y 中,您需要缓冲 Y 中的所有内容或丢弃它。目前还不清楚 OP 需要在这里做什么。
  • 假设您将按时间顺序迭代文件,这是一个非常严重的错误。这不是文件系统的工作方式。当文件从备份中删除或恢复时,灾难就会发生。保证在某个下雨天发生。对于磁盘上的数十万个文件,没有实用的解决方法,只有数据库才能让您到达某个地方。
  • var it = allTheData.GetEnumerator();应该在using 子句中使用,否则我猜它可能会泄漏 IO 句柄。

标签: c# xml performance linq bigdata


【解决方案1】:

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

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

这个方法只是检查完成,调用 MoveNext,如果 movenext 返回 false,则产生 Current 和更新完成。

然后,您通过对该方法的后续调用拆分您的集合,并使用以下方法对其进行部分枚举: TakeWhile 任何 第一的 ... 然后您可以在此基础上进行任何操作,但每个操作都必须枚举到最后。

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这三种变量引用和方法本身应该封装在一个上下文类中。

【讨论】:

  • 抱歉格式化。从一个小小的安卓屏幕发布。
猜你喜欢
  • 2019-09-04
  • 2012-12-19
  • 1970-01-01
  • 1970-01-01
  • 2013-05-25
  • 2013-12-06
  • 2012-05-26
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多