Ability to reset IEnumerator generated using yield (C#)

后端 未结 5 2119
情歌与酒
情歌与酒 2021-02-05 08:00

If I use yield instead of manually creating an IEnumerator, is it possible to implement IEnumerator.Reset?

相关标签:
5条回答
  • 2021-02-05 08:25

    This code implements an attribute that can be set to reset the enumerator. Notes:

    • Unfortunately, this method of resetting won't work with multiple enumerators instances
    • This doesn't reset it via the interface, but actually via setting a property either directly or through a method
    • Given the complexity and the fragility, I actually wouldn't recommend this approach
    • I'm actually wondering if a goto might be neater

    Code:

    public IEnumerator<Entry> GetEnumerator(){
        do{
            this.shouldReset=FALSE;
            for (Entry e = this.ReadEntry(); e != null; e = this.ReadEntry()){
                if(self.shouldReset)break;
                else yield return e;
            }
        }
        while(self.shouldReset)
    }
    
    0 讨论(0)
  • 2021-02-05 08:31

    No, it is not possible. When the C# compiler processes an iterator (a method that contains a yield statement), the compiler generates a class that implements IEnumerable and IEnumerator. The generated class' implementation of Reset just throws a NotSupportedException. There is no way to influence this in current versions of C#.

    Instead, your calling code will need to request a new enumerator, i.e. begin a new foreach loop. Or you will need to forgo the language support (the yield statement) and write your own class which implements IEnumerator.

    0 讨论(0)
  • 2021-02-05 08:45

    There's a nice workaround that I just discovered. Make your generator method return IEnumerable, not IEnumerator. Then you can do

    var values = MyGeneratorMethod();
    var enumerator = values.GetEnumerator();
    // ... do stuff with enumerator
    enumerator = values.GetEnumerator(); // instead of enumerator.Reset();
    

    I believe itowlson's answer suggested this exact trick, but I couldn't understand it until I heard of the trick elsewhere.

    0 讨论(0)
  • 2021-02-05 08:47

    There is no built-in support, but you can define your own implementation of IEnumerator that delegates all method calls to the enumerator generated by C# and only lets you define your own behavior for the Reset method.

    The simplest version of the class would look like this:

    class ResetableEnumerator<T> : IEnumerator<T>
    {
      public IEnumerator<T> Enumerator { get; set; }
      public Func<IEnumerator<T>> ResetFunc { get; set; }
    
      public T Current { get { return Enumerator.Current; } }
      public void  Dispose() { Enumerator.Dispose(); }
      object IEnumerator.Current { get { return Current; } }
      public bool  MoveNext() { return Enumerator.MoveNext(); }
      public void  Reset() { Enumerator = ResetFunc(); }
    }
    

    In this case, the ResetFunc that you specify returns a new IEnumerator<T>, so your provided implementation of ResetFunc can do some cleanup or whatever you need to do when resetting and then return a new enumerator.

    IEnumerator<int> Foo() { /* using yield return */ }
    IEnumerator<int> PublicFoo() {
      return new ResetableEnumerator<int> { 
        Enumerator = Foo(),
        ResetFunc = () => { 
          Cleanup();
          return Foo(); } };
    }
    

    You'll need to store all the originally local variables of the Foo method as fields of the class, so that you can access them in Cleanup (Note that the rest of the Foo body will never be executed after calling Reset), but that's still easier than writing a handwritten iterator!

    0 讨论(0)
  • 2021-02-05 08:47

    My solution to this problem is to use a Wrapper class that can wrap any yielding function and reset:

    public class Resettable : IEnumerator
    {
      private IEnumerator inner;
      private Func<IEnumerator> func;
    
      public Resettable(Func<IEnumerator> func)
      {
        this.func = func;
        inner = func.Invoke();
      }
    
      public bool MoveNext()
      {
        return inner.MoveNext();
      }
    
      public object Current { get { return inner.Current; } }
    
      public void Reset()
      {
        inner = func.Invoke();
      }
    
      public static Resettable Make(Func<IEnumerator> func)
      {
        return new Resettable(func);
      }
    
      public static Resettable Make<T1>(Func<T1, IEnumerator> func, T1 a1)
      {
        Func<IEnumerator> capture = () => { return func.Invoke(a1); };
        return new Resettable(capture);
      }
    
      public static Resettable Make<T1, T2>(Func<T1, T2, IEnumerator> func, T1 a1, T2 a2)
      {
        Func<IEnumerator> capture = () => { return func.Invoke(a1, a2); };
        return new Resettable(capture);
      }
    
      public static Resettable Make<T1, T2, T3>(Func<T1, T2, T3, IEnumerator> func, T1 a1, T2 a2, T3 a3)
      {
        Func<IEnumerator> capture = () => { return func.Invoke(a1, a2, a3); };
        return new Resettable(capture);
      }
    
      // add more of these to support more arguments
    }
    

    then, instead of calling your yielding method directly, you call Resettable.Make(YourMethod, ..arguments..);

    so, here's an example usage:

    public class Program
    { 
      static IEnumerator YieldTest(int start)
      {
        yield return start;
        yield return start + 1;
        yield return start + 2;
      }
        
      public static void Main()
      {
        // IEnumerator en = YieldTest(5);
        IEnumerator en = Resettable.Make(YieldTest, 5);
    
        while(en.MoveNext())
        {
          Console.WriteLine("value: "+en.Current.ToString());
        }
        Console.WriteLine("Reset");
        en.Reset();
        while(en.MoveNext())
        {
          Console.WriteLine("value: "+en.Current.ToString());
        }
      }
    }
    

    Output:

    value: 5
    value: 6
    value: 7
    Reset
    value: 5
    value: 6
    value: 7
    
    0 讨论(0)
提交回复
热议问题