确保延迟执行将只执行一次,否则

Ensure deferred execution will be executed only once or else

我 运行 遇到了一个奇怪的问题,我想知道我应该怎么做。

我有这个 class 那 return 一个 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() 并以一点开销为代价摆脱延迟执行。

我还可以在消费者中添加单元测试 class 以确保他们只调用一次,但这不会阻止将来编码的任何新消费者遇到这个潜在问题。

我想到的另一件事是使后续执行抛出。 像

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

您可以使用以下 class:

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 一个可枚举的,这样它只能被枚举一次。之后它会抛出异常。

您可以这样使用 class:

return new JustOnceOrElseEnumerable(
   myStuff.Where(...)
   ...
   );

请注意,我不推荐这种方法,因为它违反了 IEnumerable 接口的约定,因此违反了 Liskov Substitution Principle。本合同的消费者可以合法地假设他们可以根据需要多次枚举可枚举项。

相反,您可以使用缓存枚举结果的缓存枚举。这确保可枚举仅被枚举一次,并且所有后续枚举尝试都将从缓存中读取。有关详细信息,请参阅此处

您只需将您的方法转换为 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(...);
});

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;
    }
}
  • 朴素:✔ 延迟,✘ 单枚举。
  • ToList:✘ 延迟,✔ 单枚举。
  • Memoize: ✔ 延迟,✔ 单枚举。

.

编辑 以使用正确的术语并充实实施。