使用条件将一次性使用的大型 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.
中
假设我们有一个 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.