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

时间:2016-11-10 21:41:42

标签: c# linq lambda deferred-execution

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

我有一个返回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
    }
}

3 个答案:

答案 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;
    }
}
  • 天真:✔延期,✘单枚举。
  • ToList:✘延期,✔单枚举。
  • 回忆:✔延期,✔单枚举。

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