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