【问题标题】:When IEnumerator.Reset() method is called?何时调用 IEnumerator.Reset() 方法?
【发布时间】:2011-04-26 06:08:36
【问题描述】:

让我们有这个代码:

class MyList : IEnumerable, IEnumerator
{
    int[] A = { 1, 2, 3, 4, 5 };
    int i = -1;

    #region IEnumerator Members

    public object Current
    {
        get { return A[i]; }
    }

    public bool MoveNext()
    {
        i++;
        return i < 5;
    }

    public void Reset()
    {
        i = -1;
    }

    #endregion

    #region IEnumerable Members

    public IEnumerator GetEnumerator()
    {
        return (IEnumerator)this;
    }

    #endregion
}

在主要方法中:

MyList list = new MyList();
foreach (int i in list)
{
    Console.WriteLine(i);
}

foreach (int i in list)
{
    Console.WriteLine(i);
}

为什么第二个 foerach 不起作用?并且“i”不再初始化?

这是真的吗:在执行 foreach 之前应该自动调用 Reset 方法?

为什么它不在这里调用?

【问题讨论】:

  • 没有办法解决吗?? :(
  • 我认为它只存在于与 COM 的互操作性。普通的.net代码不使用它,几乎所有的实现都会抛出异常。

标签: c# interface


【解决方案1】:

重置是多余的;以至于迭代器块在重置时抛出异常是语言规范中的要求。正确的做法是简单地处理和释放旧的迭代器,然后再次调用 GetEnumerator。或者更好:避免读取两次,因为并非所有数据都是可重复的。

【讨论】:

  • 你有链接到它说应该抛出异常的地方吗?我想了解一下。
  • 嗯...找到this。显然Reset 只存在于 COM 互操作性。在我看来,一开始就不应该在接口上定义它。
  • 如果定义得更好,重置不会是多余的。如果有一个“通用”IEnumerable,以及一个从它继承并返回一个实现 Reset 的 IMultipassEnumerator 的 IMultipassEnumerable,那么它可能具有这样的语义,即使用相同的枚举器重复枚举将保证返回相同的结果,即使收藏已更改;相比之下,如果尝试使用两个单独的枚举器,它们很可能会返回不同的结果。
  • @supercat 我不反对,但是:没有:p
【解决方案2】:

IEnumerable 和 IEnumerator 通常应该是单独的类,并且除了始终返回空或始终返回相同项的枚举器的情况外,GetEnumerator 方法必须始终返回 IEnumerator 的新实例。

IEnumerator.Reset 没什么意义; for-each 循环不使用它,IEnumerable/IEnumerator 的使用者不能使用它,除非他们知道可枚举类型是什么,在这种情况下,他们可以使用实际类型而不是接口。

【讨论】:

  • 如果有人以这样的方式实现 MoveNext 方法,当它返回 false 时,在此调用之前重置是一种不好的做法?如果是这样,为什么他们将重置放入界面?
  • @jannagy02:.NET 的许多方面应被视为进化/或妥协的残余产物。有些从来没有经过深思熟虑;其他部分在设计的某个阶段可能是明智和有用的部分,但在设计的其他部分返工时变得无用。有时事情会在设计过程中演变,即使是那些参与设计的人也可能不记得哪些部分是经过深思熟虑的,哪些是拼凑在一起的。
【解决方案3】:

foreach 未调用重置。查看 Reflector 中的 Main 方法可以确认这一点。

.NET 类,如 ArrayList,实际上返回实现 IEnumerator 的类的新实例。

例如ArrayList实现IEnumerable,其GetEnumerator方法如下所示:

public virtual IEnumerator GetEnumerator()
{
    return new ArrayListEnumeratorSimple(this);
}

因此无需担心调用 Reset,因为每个 foreach 都使用枚举器的新实例。

有关显示 IEnumerable 实现的完整示例和实现 IEnumerator 的单独类,您可以查看IEnumerable 的文档。

【讨论】:

    【解决方案4】:

    这也有效:

    public bool MoveNext()
    {
            if(i < 5)
            {
                i++;
                return true;
            }
            else
            {
                i = -1;
                return false;
            }
    }
    

    【讨论】:

      【解决方案5】:

      虽然我同意 cmets 关于残留片段的观点,并且框架代码不使用 Reset() 并且生成器块确实会引发异常,但我不同意它完全没用。我对生成器块为何在重置时抛出异常的理解是因为对任何构建状态的担忧。生成器块的本质使它们特别不适合可重置操作,但这并不意味着可重置枚举器不好或设计不佳。

      考虑一个复杂的枚举,它由数百个不同的“装饰器”枚举器组成。构建这样一个对象图的成本是不可忽略的。现在还要考虑这个复杂枚举的来源是动态的,但是出于处理的原因,需要快照语义。在这种情况下,我们可以创建这个“枚举器堆栈”,并在第一次调用MoveNext() 时拍摄源快照。执行复杂的枚举/投影/等并获得结果。现在我们希望再次执行此操作,从头开始。 Reset() 为整个枚举器堆栈提供了一种机制,可以重新初始化到它的起始状态,而无需重新构建整个对象图。此外,它允许我们将对象图的构建与需要多次运行此复杂枚举的消费者分开。

      我发现使用可重置枚举器有很多用途,而且它几乎总是与某种需要复杂/可组合枚举器装饰器的数据馈送相关。在许多装饰器中,对Reset() 的调用只是传递给包装的枚举器实例,但在其他装饰器中,会执行一些次要工作,例如将运行总和归零,可能重新启动开始时间,或重新获取源数据快照。

      下面是一个源枚举器示例,它下载一个列表中的文档,然后在该列表上进行枚举。重置枚举器会导致重新下载列表。此外,定义了一个装饰器枚举器,可用于包装列表枚举器(或任何枚举器)以投影枚举器的项目。我称它为SelectEnumerator,因为它的作用与Enumerable.Select相同

      // excuse the poorly named types
      public class ListDownloaderEnumerator<T> : IEnumerator<T>
      {
          private int index = -1;
          private readonly string url;
          private IReadOnlyList<T> items;
          public ListDownloaderEnumerator(string url)
          {
              this.url = url;
          }
          public bool MoveNext()
          {
              // downloading logic removed for brevity
              if (items == null) download(url);
              index = index + 1;
              return index < items.Count;
          }
          public void Reset()
          {
              index = -1;
              items = null;
          }
          // other parts of IEnumerator<T>, such as Current
      }
      
      public class SelectEnumerator<T, TResult> : IEnumerator<T>
      {
          private readonly IEnumerator<T> enumerator;
          private readonly Func<T, TResult> projection;
          public SelectEnumerator(IEnumerator<T> enumerator, Func<T, TResult> projection)
          {
              this.enumerator = enumerator;
              this.projection = projection;
          }
          public bool MoveNext()
          {
              return enumerator.MoveNext();
          }
          public void Reset()
          {
              enumerator.Reset();
          }
          // other parts of IEnumerator<T>, such as Current
      }
      
      // somewhere else in the application
      // we can now write processing code without concern for sourcing
      // and perhaps projecting the data. this example is very simple,
      // but using decorator enumerator you can accomplish very complex
      // processing of sequences while maintaining small, testable, and
      // composable classes. it also allows for highly configurable
      // processing, since the decorators become building blocks.
      public class DownloadedDataProcessor
      {
          private readonly IEnumerator<MyProjectedListItem> enumerator;
          public DownloadedDataProcessor(IEnumerator<MyProjectedListItem> enumerator)
          {
              this.enumerator = enumerator;
          }
          public void ProcessForever()
          {
              while (true)
              {
                  while (enumerator.MoveNext())
                  {
                      Process(enumerator.Current);
                  }
      
                  enumerator.Reset();
              }
          }
          private void Process(MyProjectedListItem item)
          {
              // top secret processing
          }
      }
      
      

      【讨论】:

        【解决方案6】:

        在很多情况下我都需要这样做,所以我要做的是在我的 GetEnumerator 方法中调用 reset。这是一个例子:

        public IEnumerator GetEnumerator()
        {
            this.Reset(); // Reset each time you get the Enumerator
            return (IEnumerator)this;
        }
        

        【讨论】:

        • 接受的答案要好得多;您根本不应该让您的IEnumerable 每次都返回相同的IEnumerator 实例。使用这种方法,您不能永远让两个不同的迭代器迭代同一个列表。使用具有多个迭代器的正确实现根本不是问题。
        • 这很好。我对原始问题的回答更直接,该问题是两次迭代集合。
        猜你喜欢
        • 2013-10-06
        • 2011-08-23
        • 2015-01-08
        • 1970-01-01
        • 2013-03-13
        • 2011-09-06
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多