Чи є у ітератора договір, що не має руйнування?


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. Це просто дивно повторювати цей тип замовленої колекції без виконання звичайних / руйнівних операцій.
Стівен Еверс

1

Однією з найцікавіших проблем кодування, що була надана мені на інтерв'ю, було створення функціональної черги. Вимога полягала в тому, щоб кожен виклик для запитування створював нову чергу, яка містила стару чергу та новий елемент у хвіст. Dequeue також повертає нову чергу та вилучений елемент як параметр.

Створення IEnumerator з цієї реалізації було б не руйнівним. І дозвольте мені сказати, що реалізувати функціональну чергу, яка спрацьовує добре, набагато складніше, ніж реалізувати функціональний стек функціональних (стек Push / Pop обидва працюють на Tail, бо Queque Enqueue працює на хвості, dequeue працює на голові).

Моя думка - це банально створити неруйнівний перелік стеків, реалізуючи власний механізм покажчика (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; комплектування дасть вам інший стек для перерахунку, ніж якби не було багатопотокових читань (ви можете використовувати ReaderWriterLock для захисту від цього). Я зробив поля в StackNode лише заново, але, звичайно, якщо T є об'єктом, що змінюється, ви можете змінити його значення. Якщо ви створили конструктор Stack, який взяв StackNode в якості параметра (і встановив голову до того, що передається у вузол). Дві складені таким чином стеки не впливатимуть один на одного (за винятком змінного T, як я вже згадував). Ви можете натискати та виконувати все, що вам потрібно, в одній стеці, інша не зміниться.

І що мій друг - це те, як ти робиш неруйнівне перерахування Стек.


+1: Мої неруйнівні перелік стеків та черг реалізовані аналогічно. Я думав більше по лінії PQs / Heaps із замовними порівняльниками.
Стівен Еверс

1
Я б сказав, що використовую той самий підхід ... не можу сказати, що я раніше застосував функціональну 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>те, що може дати гарантій, таких типів ще не існує.


0

Я думаю, це було б порушенням принципу заміни Ліскова, який стверджує,

Об'єкти в програмі повинні бути замінені екземплярами їх підтипів, не змінюючи правильності цієї програми.

Якби у мене в моїй програмі був такий метод, як наступний, який викликається не один раз,

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

Я повинен мати можливість замінити будь-який номер IE без зміни коректності програми, але використання деструктивної черги значно змінить поведінку програм.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.