确保延迟执行只执行一次

本文关键字:执行 一次 延迟 确保 | 更新日期: 2023-09-27 18:17:25

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

我有这个类,它返回一个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
    }
}

确保延迟执行只执行一次

您可以通过简单地将方法转换为迭代器来解决不正确的结果问题:

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(...);
});

您可以使用以下类:

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();
    }
}

修饰一个可枚举对象,使其只能被枚举一次。之后,它会抛出一个异常。

你可以这样使用这个类:

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

请注意,我不推荐这种方法,因为它违反了IEnumerable接口的契约,因此违反了Liskov替代原则。对于该契约的消费者来说,假设他们可以随意枚举可枚举对象,这是合法的。

相反,您可以使用缓存的枚举对象来缓存枚举结果。这可确保枚举对象仅枚举一次,并且所有后续枚举尝试都将从缓存中读取。

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:✔Deferred,✔Single enumeration。

.

编辑以使用适当的术语并充实实现。