我遇到了一个奇怪的问题,我想知道我应该怎么做。
我有一个返回IEnumerable<MyClass>
的类,它是一个延迟执行。现在,有两个可能的消费者。其中一人对结果进行了分类。
请参阅以下示例:
public class SomeClass
{
public IEnumerable<MyClass> GetMyStuff(Param givenParam)
{
double culmulativeSum = 0;
return myStuff.Where(...)
.OrderBy(...)
.TakeWhile( o =>
{
bool returnValue = culmulativeSum < givenParam.Maximum;
culmulativeSum += o.SomeNumericValue;
return returnValue;
};
}
}
消费者只调用延迟执行一次,但是如果他们要调用更多,则结果将是错误的,因为culmulativeSum
不会被重置。我通过单元测试无意中发现了这个问题。
我解决问题的最简单方法是添加.ToArray()
并以一点点开销为代价摆脱延迟执行。
我还可以在消费者类中添加单元测试,以确保它们只调用一次,但这不会阻止将来编码这个潜在问题的新消费者。
我想到的另一件事是进行后续执行。 像
这样的东西return myStuff.Where(...)
.OrderBy(...)
.TakeWhile(...)
.ThrowIfExecutedMoreThan(1);
显然这不存在。 实现这样的事情是一个好主意,你会怎么做?
否则,如果有一只我没看到的大粉红色大象,指出它将会受到赞赏。 (我觉得有一个,因为这个问题是关于一个非常基本的场景:|)
这是一个糟糕的消费者使用示例:
public class ConsumerClass
{
public void WhatEverMethod()
{
SomeClass some = new SomeClass();
var stuffs = some.GetMyStuff(param);
var nb = stuffs.Count(); //first deferred execution
var firstOne = stuff.First(); //second deferred execution with the culmulativeSum not reset
}
}
答案 0 :(得分:11)
只需将方法转换为iterator:
即可解决错误的结果问题double culmulativeSum = 0;
var query = myStuff.Where(...)
.OrderBy(...)
.TakeWhile(...);
foreach (var item in query) yield return item;
它可以封装在一个简单的扩展方法中:
public static class Iterators
{
public static IEnumerable<T> Lazy<T>(Func<IEnumerable<T>> source)
{
foreach (var item in source())
yield return item;
}
}
然后,在这种情况下你需要做的就是用Iterators.Lazy
调用来包围原始方法体,例如:
return Iterators.Lazy(() =>
{
double culmulativeSum = 0;
return myStuff.Where(...)
.OrderBy(...)
.TakeWhile(...);
});
答案 1 :(得分:6)
您可以使用以下课程:
public class JustOnceOrElseEnumerable<T> : IEnumerable<T>
{
private readonly IEnumerable<T> decorated;
public JustOnceOrElseEnumerable(IEnumerable<T> decorated)
{
this.decorated = decorated;
}
private bool CalledAlready;
public IEnumerator<T> GetEnumerator()
{
if (CalledAlready)
throw new Exception("Enumerated already");
CalledAlready = true;
return decorated.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
if (CalledAlready)
throw new Exception("Enumerated already");
CalledAlready = true;
return decorated.GetEnumerator();
}
}
到decorate一个可枚举的内容,以便只能枚举一次。之后会抛出异常。
您可以像这样使用此类:
return new JustOnceOrElseEnumerable(
myStuff.Where(...)
...
);
请注意,我不推荐这种方法,因为它违反了IEnumerable
界面的合同,因而违反了Liskov Substitution Principle。对于本合同的消费者来说,假设他们可以根据自己的喜好列举可枚举的数量是合法的。
相反,您可以使用缓存枚举结果的缓存枚举。这确保了枚举只被枚举一次,并且所有后续枚举尝试都将从缓存中读取。有关详细信息,请参阅此处this answer。
答案 2 :(得分:4)
Ivan的答案非常适合OP示例中的基本问题 - 但对于一般情况,我过去使用类似于下面的扩展方法来解决这个问题。这可以确保Enumerable具有单个评估,但也是延迟的:
public static IMemoizedEnumerable<T> Memoize<T>(this IEnumerable<T> source)
{
return new MemoizedEnumerable<T>(source);
}
private class MemoizedEnumerable<T> : IMemoizedEnumerable<T>, IDisposable
{
private readonly IEnumerator<T> _sourceEnumerator;
private readonly List<T> _cache = new List<T>();
public MemoizedEnumerable(IEnumerable<T> source)
{
_sourceEnumerator = source.GetEnumerator();
}
public IEnumerator<T> GetEnumerator()
{
return IsMaterialized ? _cache.GetEnumerator() : Enumerate();
}
private IEnumerator<T> Enumerate()
{
foreach (var value in _cache)
{
yield return value;
}
while (_sourceEnumerator.MoveNext())
{
_cache.Add(_sourceEnumerator.Current);
yield return _sourceEnumerator.Current;
}
_sourceEnumerator.Dispose();
IsMaterialized = true;
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public List<T> Materialize()
{
if (IsMaterialized)
return _cache;
while (_sourceEnumerator.MoveNext())
{
_cache.Add(_sourceEnumerator.Current);
}
_sourceEnumerator.Dispose();
IsMaterialized = true;
return _cache;
}
public bool IsMaterialized { get; private set; }
void IDisposable.Dispose()
{
if(!IsMaterialized)
_sourceEnumerator.Dispose();
}
}
public interface IMemoizedEnumerable<T> : IEnumerable<T>
{
List<T> Materialize();
bool IsMaterialized { get; }
}
使用示例:
void Consumer()
{
//var results = GetValuesComplex();
//var results = GetValuesComplex().ToList();
var results = GetValuesComplex().Memoize();
if(results.Any(i => i == 3))
{
Console.WriteLine("\nFirst Iteration");
//return; //Potential for early exit.
}
var last = results.Last(); // Causes multiple enumeration in naive case.
Console.WriteLine("\nSecond Iteration");
}
IEnumerable<int> GetValuesComplex()
{
for (int i = 0; i < 5; i++)
{
//... complex operations ...
Console.Write(i + ", ");
yield return i;
}
}
已编辑以使用正确的术语并充实实施。