Есть ли у итератора неразрушающий подразумеваемый контракт?

11

Допустим, я проектирую собственную структуру данных, такую ​​как стек или очередь (например, - это может быть какая-то другая произвольная упорядоченная коллекция, которая имеет логический эквивалент pushи popметоды - то есть деструктивные методы доступа).

Если бы вы реализовывали итератор (в частности, в .NET IEnumerable<T>) для этой коллекции, которая появлялась на каждой итерации, это нарушило бы IEnumerable<T>подразумеваемый контракт?

Имеет ли IEnumerable<T>это подразумеваемый контракт?

например:

public IEnumerator<T> GetEnumerator()
{
    if (this.list.Count > 0)
        yield return this.Pop();
    else
        yield break;
}
Стивен Эверс
источник

Ответы:

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

Одной из самых интересных задач, которые мне дали для интервью, было создание функциональной очереди. Требованием было то, что каждый вызов в очередь создает новую очередь, которая содержит старую очередь и новый элемент в хвосте. Dequeue также возвращает новую очередь и удаленный элемент как выходной параметр.

Создание IEnumerator из этой реализации было бы неразрушающим. И позвольте мне сказать вам, что реализация функциональной очереди, которая работает хорошо, намного сложнее, чем реализация производительного функционального стека (стек Push / Pop работают как в хвосте, так как очередь очереди работает в хвосте, очередь работает в голове).

Суть в том, что ... создать неразрушающий перечислитель стека тривиально, реализовав собственный механизм указателя (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;
    }
  }    
}

Несколько вещей, чтобы отметить. Вызов push или pop перед оператором current = _head; Завершение дало бы вам другой стек для перечисления, чем если бы не было многопоточности (вы можете использовать ReaderWriterLock для защиты от этого). Я сделал поля в StackNode доступными только для чтения, но если, конечно, T является изменяемым объектом, вы можете изменить его значения. Если вы должны были создать конструктор Stack, который взял бы StackNode в качестве параметра (и установил head для того, что было передано в узел). Два стека, построенные таким образом, не будут влиять друг на друга (за исключением изменяемого T, как я уже упоминал). Вы можете Push и Pop все, что вы хотите в одном стеке, другой не изменится.

И это мой друг, как вы делаете неразрушающее перечисление стека.

Майкл Браун
источник
+1: Мои неразрушающие счетчики стека и очереди реализованы аналогично. Я больше думал о PQ / Heaps с пользовательскими компараторами.
Стивен Эверс
1
Я бы сказал, что использую тот же подход ... не могу сказать, что я реализовал Functional PQ раньше, хотя ... похоже, что эта статья предлагает использовать упорядоченные последовательности в качестве нелинейного приближения
Майкл Браун
0

В попытке сделать вещи «простыми», .NET имеет только одну универсальную / неуниверсальную пару интерфейсов для вещей, которые перечисляются путем вызова, GetEnumerator()а затем с использованием MoveNextи Currentна полученном от них объекте, хотя существует по крайней мере четыре вида объектов который должен поддерживать только те методы:

  1. Вещи, которые можно перечислить хотя бы один раз, но не обязательно более того.
  2. Вещи, которые можно перечислять произвольное количество раз в контексте с произвольной резьбой, но каждый раз могут произвольно выдавать разное содержимое
  3. Вещи, которые можно перечислить произвольное количество раз в контекстах со свободной резьбой, но могут гарантировать, что если код, который их перечисляет многократно, не вызывает никаких методов мутации, все перечисления будут возвращать один и тот же контент.
  4. Вещи, которые могут быть перечислены произвольное количество раз и гарантированно возвращать одно и то же содержимое каждый раз, пока они существуют.

Любой экземпляр, который удовлетворяет одному из определений с более высокими номерами, также будет удовлетворять всем более низким, но код, который требует объекта, удовлетворяющего одному из более высоких определений, может сломаться, если ему дано одно из более низких определений.

Похоже, что Microsoft решила, что классы, которые реализуют, IEnumerable<T>должны удовлетворять второму определению, но не обязаны удовлетворять чему-то более высокому. Возможно, нет особой причины, по которой что-то, что могло бы соответствовать только первому определению, должно быть реализовано, IEnumerable<T>а не IEnumerator<T>; если бы foreachциклы могли принять IEnumerator<T>, было бы разумно для вещей, которые можно перечислить только один раз, просто реализовать последний интерфейс. К сожалению, типы, которые только реализуют IEnumerator<T>, менее удобны для использования в C # и VB.NET, чем типы, у которых есть GetEnumeratorметод.

В любом случае, даже если было бы полезно, если бы существовали разные перечислимые типы для вещей, которые могли бы давать разные гарантии, и / или стандартный способ запроса экземпляра, который реализует, IEnumerable<T>какие гарантии он может дать, таких типов еще не существует.

Supercat
источник
0

Я думаю, что это будет нарушением принципа подстановки Лискова, который гласит:

объекты в программе должны заменяться экземплярами их подтипов без изменения правильности этой программы.

Если бы у меня был метод, подобный следующему, который вызывается в моей программе более одного раза,

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

Я должен быть в состоянии поменять любой IEnumerable без изменения правильности программы, но использование деструктивной очереди сильно изменит поведение программ.

Деспертар
источник