迭代器是否具有非破坏性的隐含合同?


11

假设我正在设计一个自定义数据结构,如堆栈或队列(例如-可能是其他任意有序集合,其逻辑等效于pushand pop方法-即破坏性访问器方法)。

如果您要IEnumerable<T>在每次迭代中弹出的这个集合上实现一个迭代器(特别是在.NET中),那会打破IEnumerable<T>Breaking的隐含契约吗?

是否IEnumerable<T>有此隐含合同?

例如:

public IEnumerator<T> GetEnumerator()
{
    if (this.list.Count > 0)
        yield return this.Pop();
    else
        yield break;
}

Answers:


12

我认为破坏性的枚举器违反了“最小惊讶原则”。作为一个人为的示例,想象一个提供通用便利功能的业务对象库。我天真地写了一个更新业务对象集合的函数:

static void UpdateStatus<T>(IEnumerable<T> collection) where T : IBusinessObject
{
    foreach (var item in collection)
    {
        item.Status = BusinessObjectStatus.Foo;
    }
}

另一个对我的实现或您的实现一无所知的开发人员无辜地决定同时使用这两者。他们可能会对结果感到惊讶:

//developer uses your type without thinking about the details
var collection = GetYourEnumerableType<SomeBusinessObject>();

//developer uses my function without thinking about the details
UpdateStatus<SomeBusinessObject>(collection);

即使开发人员知道破坏性枚举,将集合移交给黑盒功能时,他们也可能不会想到后果。作为的作者UpdateStatus,我可能不会在设计中考虑破坏性枚举。

但是,这只是一个隐含合同。.NET集合(包括)Stack<T>与它们强制执行显式协定InvalidOperationException-“在枚举器实例化后修改了集合”。您可能会争辩说,一个真正的专业人员对任何不是他们自己的代码都怀有告诫的态度。破坏性枚举器的意外发现将通过最少的测试来发现。


1
+1-对于“最少惊讶的原理”,我将在人们身上引用。

+1这基本上也是我在想的,而且,破坏性的行为可能会破坏LINQ IEnumerable扩展的预期行为。在不执行常规/破坏性操作的情况下,对这种有序集合进行迭代只是感觉很奇怪。
史蒂文·埃弗斯

1

面试中给我的最有趣的编码挑战之一是创建一个功能队列。要求是,每次入队调用都将创建一个新队列,该队列包含旧队列和尾部的新项目。出队也将返回新队列,并将出队的项目作为出参数。

从此实现中创建IEnumerator将是非破坏性的。而且,让我告诉您,要实现性能良好的功能性队列要比实现性能良好的功能性栈困难得多(堆栈Push / Pop都在尾部工作,因为队列入队在尾部工作,出队在头上工作)。

我的意思是……通过实现自己的Pointer机制(StackNode <T>)并在枚举器中使用函数语义来创建无损堆栈枚举器是微不足道的。

public class Stack<T> implements IEnumerator<T>
{
  private class StackNode<T>
  {
    private readonly T _data;
    private readonly StackNode<T> _next;
    public StackNode(T data, StackNode<T> next)
    {
       _data=data;
       _next=next;
    }
    public <T> Data{get {return _data;}}
    public StackNode<T> Next{get {return _Next;}}
  }

  private StackNode<T> _head;

  public void Push(T item)
  {
    _head =new StackNode<T>(item,_head);
  }

  public T Pop()
  {
    //Add in handling for a null head (i.e. fresh stack)
    var temp=_head.Data;
    _head=_head.Next;
    return temp;
  }

  ///Here's the fun part
  public IEnumerator<T> GetEnumerator()
  {
    //make a copy.
    var current=_head;
    while(current!=null)
    {
       yield return current.Data;
       current=_head.Next;
    }
  }    
}

一些注意事项。在语句current = _head之前调用push或pop的调用;如果没有多线程,则完成将为您提供不同的枚举堆栈(您可能需要使用ReaderWriterLock来防止这种情况)。我将StackNode中的字段设置为只读,但如果T是可变对象,则可以更改其值。如果要创建一个将StackNode作为参数的Stack构造函数(并将其头设置为传入的节点)。以这种方式构造的两个堆栈不会相互影响(我提到的可变T除外)。您可以将所需的所有内容推入并弹出,而其他则不会改变。

我的朋友就是您如何对Stack进行无损枚举。


+1:我的非破坏性堆栈和队列枚举器的实现方式类似。我在使用自定义比较器根据PQ /堆进行更多思考。
史蒂文·埃弗斯

1
我会说使用相同的方法...虽然不能说我已经实现了功能性PQ ...看起来像本文建议使用有序序列作为非线性近似值
Michael Brown

0

为了使事物保持“简单”,.NET仅具有一对通用/非通用接口,用于通过调用GetEnumerator()然后使用MoveNextCurrent对从其接收的对象进行枚举的事物,即使至少存在四种对象应该只支持那些方法:

  1. 可以至少枚举一次的事物,但不一定要枚举更多。
  2. 在自由线程上下文中可以枚举任意次数的事物,但每次都可以任意产生不同的内容
  3. 在自由线程上下文中可以枚举任意次数的事物,但是可以保证,如果反复枚举它们的代码不调用任何变异方法,则所有枚举将返回相同的内容。
  4. 可以枚举任意次的事物,只要存在就可以保证每次都返回相同的内容。

满足编号较高的定义之一的任何实例也将满足所有较低的定义,但是如果给定较低的定义之一,则要求对象满足较高定义之一的代码可能会中断。

看来Microsoft决定实现的类IEnumerable<T>应满足第二个定义,但没有义务满足更高的要求。可以说,没有什么理由只能实现第一个定义IEnumerable<T>而不是IEnumerator<T>; 如果foreach循环可以接受IEnumerator<T>,那么对于那些只能被枚举一次以简单实现后者的接口来说,这是有意义的。不幸的是,IEnumerator<T>与具有GetEnumerator方法的类型相比,仅实现的类型在C#和VB.NET中使用不方便。

无论如何,即使对于可以做出不同保证的事物使用不同的可枚举类型,和/或以标准方式询问实现IEnumerable<T>其可以做出何种保证的实例,这将是有用的,但尚无此类类型。


0

我认为这违反了利斯科夫(Liskov)替换原则,该原则指出,

程序中的对象应该可以用其子类型的实例替换,而不会改变该程序的正确性。

如果我有一个类似以下方法的程序在我的程序中多次调用,

PrintCollection<T>(IEnumerable<T> collection)
{
    foreach (T item in collection)
    {
        Console.WriteLine(item);
    }
}

我应该能够在不改变程序正确性的情况下换出任何IEnumerable,但是使用破坏性队列将极大地改变程序的行为。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.