Un iteratore ha un contratto implicito non distruttivo?


11

Diciamo che sto progettando una struttura di dati personalizzata come uno stack o una coda (ad esempio - potrebbe essere un'altra raccolta arbitraria ordinata che ha l'equivalente logico di pushe popmetodi - ovvero metodi di accesso distruttivi).

Se si stesse implementando un iteratore (in .NET, in particolare IEnumerable<T>) su questa raccolta che è spuntato su ogni iterazione, ciò avrebbe infranto IEnumerable<T>il contratto implicito?

Ha IEnumerable<T>questo contratto implicito?

per esempio:

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

Risposte:


12

Credo che un enumeratore distruttivo violi il principio del minimo stupore . Ad esempio, immagina una libreria di oggetti business che offra funzioni di praticità generiche. Scrivo innocentemente una funzione che aggiorna una raccolta di oggetti business:

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

Un altro sviluppatore che non sa nulla della mia implementazione o della tua implementazione decide innocentemente di utilizzare entrambi. È probabile che saranno sorpresi dal risultato:

//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);

Anche se lo sviluppatore è a conoscenza dell'enumerazione distruttiva, potrebbe non pensare alle ripercussioni quando passa la raccolta a una funzione black-box. Come autore di UpdateStatus, probabilmente non prenderò in considerazione l'enumerazione distruttiva nel mio progetto.

Tuttavia, è solo un contratto implicito. Le raccolte .NET, incluso Stack<T>, impongono un contratto esplicito con il loro InvalidOperationException: "La raccolta è stata modificata dopo l'istanza dell'enumeratore". Si potrebbe sostenere che un vero professionista ha un atteggiamento emettitore nei confronti di qualsiasi codice che non è il proprio. La sorpresa di un enumeratore distruttivo verrebbe scoperta con test minimi.


1
+1 - per "Principio del minimo stupore" lo citerò alla gente.

+1 Questo è fondamentalmente anche quello che stavo pensando, inoltre, essere distruttivo potrebbe interrompere il comportamento previsto delle estensioni LINQ IEnumerable. È strano scorrere questo tipo di raccolta ordinata senza eseguire le operazioni normali / distruttive.
Steven Evers,

1

Una delle sfide di programmazione più interessanti che mi è stata data per un'intervista è stata quella di creare una coda funzionale. Il requisito era che ogni chiamata per accodarsi creasse una nuova coda che contenesse la vecchia coda e il nuovo elemento in coda. Dequeue restituirebbe anche una nuova coda e l'elemento dequeued come parametro out.

La creazione di un IEnumerator da questa implementazione non sarebbe distruttiva. E lascia che ti dica che implementare una coda funzionale che funziona bene è molto più difficile che implementare una pila funzionale performante (stack Push / Pop entrambi funzionano sulla coda, perché un accodamento di coda funziona sulla coda, il dequeue funziona sulla testa).

Il mio punto di vista è ... è banale creare un Enumeratore Stack non distruttivo implementando il proprio meccanismo Puntatore (StackNode <T>) e usando la semantica funzionale nell'Enumeratore.

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;
    }
  }    
}

Alcune cose da notare Una chiamata a push o pop prima dell'istruzione current = _head; completes ti darebbe uno stack diverso per l'enumerazione rispetto a se non ci fosse multithreading (potresti voler usare un ReaderWriterLock per proteggerti da questo). Ho creato i campi in StackNode in sola lettura, ma ovviamente se T è un oggetto mutabile, puoi cambiarne i valori. Se dovessi creare un costruttore Stack che utilizza StackNode come parametro (e imposta head su quello passato nel nodo). Due pile costruite in questo modo non si influenzeranno a vicenda (ad eccezione di una T mutabile come ho già detto). Puoi Push and Pop tutto quello che vuoi in uno stack, l'altro non cambierà.

E che amico mio è come si fa l'enumerazione non distruttiva di una pila.


+1: I miei enumeratori non distruttivi di Stack e Queue sono implementati in modo simile. Stavo pensando di più sulla falsariga di PQ / Heaps con comparatori personalizzati.
Steven Evers,

1
Direi di usare lo stesso approccio ... non posso dire di aver implementato un PQ funzionale prima però ... sembra che questo articolo suggerisca di usare sequenze ordinate come approssimazione non lineare
Michael Brown

0

Nel tentativo di mantenere le cose "semplici", .NET ha solo una coppia di interfacce generiche / non generiche per le cose che sono enumerate chiamando GetEnumerator()e quindi usando MoveNexte Currentsull'oggetto da esse ricevuto, anche se ci sono almeno quattro tipi di oggetti che dovrebbe supportare solo quei metodi:

  1. Cose che possono essere elencate almeno una volta, ma non necessariamente più di così.
  2. Cose che possono essere enumerate un numero arbitrario di volte in contesti a thread libero, ma che possono produrre arbitrariamente contenuti diversi ogni volta
  3. Cose che possono essere enumerate un numero arbitrario di volte in contesti a thread libero, ma che possono garantire che se il codice che le enumera ripetutamente non chiama metodi di mutazione, tutte le enumerazioni restituiranno lo stesso contenuto.
  4. Cose che possono essere enumerate un numero arbitrario di volte e che sono garantite per restituire lo stesso contenuto ogni volta che esistono.

Qualsiasi istanza che soddisfi una delle definizioni con un numero più alto soddisferà anche tutte le definizioni inferiori, ma il codice che richiede un oggetto che soddisfa una delle definizioni più alte può rompersi se viene fornita una di quelle inferiori.

Sembra che Microsoft abbia deciso che le classi che implementano IEnumerable<T>dovrebbero soddisfare la seconda definizione, ma non hanno l'obbligo di soddisfare qualcosa di più elevato. Probabilmente, non c'è molta ragione per cui qualcosa che potrebbe soddisfare solo la prima definizione dovrebbe implementare IEnumerable<T>piuttosto che IEnumerator<T>; se i foreachloop potessero accettare IEnumerator<T>, avrebbe senso per le cose che possono essere enumerate una sola volta implementare semplicemente quest'ultima interfaccia. Sfortunatamente, i tipi che implementano solo IEnumerator<T>sono meno convenienti da usare in C # e VB.NET rispetto ai tipi che hanno un GetEnumeratormetodo.

In ogni caso, anche se sarebbe utile se esistessero tipi enumerabili diversi per cose che potrebbero offrire garanzie diverse e / o un modo standard per chiedere un'istanza che implementa le IEnumerable<T>garanzie che può fare, nessun tipo esiste ancora.


0

Penso che questa sarebbe una violazione del principio di sostituzione di Liskov che afferma,

gli oggetti in un programma dovrebbero essere sostituibili con le istanze dei loro sottotipi senza alterare la correttezza di quel programma.

Se avessi un metodo come il seguente che viene chiamato più di una volta nel mio programma,

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

Dovrei essere in grado di scambiare qualsiasi IEnumerable senza alterare la correttezza del programma, ma l'uso della coda distruttiva altererebbe ampiamente il comportamento dei programmi.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.