Un itérateur a-t-il un contrat implicite non destructif?


11

Disons que je conçois une structure de données personnalisée comme une pile ou une file d'attente (par exemple - pourrait être une autre collection ordonnée arbitrairement qui a l'équivalent logique de pushet des popméthodes - c'est-à-dire des méthodes d'accesseur destructives).

Si vous implémentiez un itérateur (en .NET, en particulier IEnumerable<T>) sur cette collection qui apparaissait à chaque itération, cela IEnumerable<T>romprait-il le contrat implicite?

A IEnumerable<T>ce contrat implicite?

par exemple:

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

Réponses:


12

Je crois qu'un énumérateur destructeur viole le principe du moindre étonnement . Par exemple, imaginez une bibliothèque d'objets métier qui offre des fonctions de commodité génériques. J'écris innocemment une fonction qui met à jour une collection d'objets métier:

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

Un autre développeur qui ne sait rien de mon implémentation ou de votre implémentation décide innocemment d'utiliser les deux. Il est probable qu'ils seront surpris par le résultat:

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

Même si le développeur est conscient de l'énumération destructrice, il peut ne pas penser aux répercussions lors de la remise de la collection à une fonction boîte noire. En tant qu'auteur de UpdateStatus, je ne vais probablement pas considérer l'énumération destructrice dans ma conception.

Cependant, ce n'est qu'un contrat implicite. Les collections .NET, notamment Stack<T>, appliquent un contrat explicite avec leur InvalidOperationException- "La collection a été modifiée après l'instanciation de l'énumérateur". Vous pourriez faire valoir qu'un véritable professionnel a une attitude de mise en garde contre tout code qui n'est pas le leur. La surprise d'un énumérateur destructeur serait découverte avec un minimum de tests.


1
+1 - pour «Principe du moindre étonnement», je vais le citer aux gens.

+1 C'est essentiellement ce à quoi je pensais aussi, être destructeur pourrait briser le comportement attendu des extensions LINQ IEnumerable. Il est tout simplement étrange d'itérer sur ce type de collection ordonnée sans effectuer les opérations normales / destructives.
Steven Evers

1

L'un des défis de codage les plus intéressants qui m'a été donné pour une interview a été de créer une file d'attente fonctionnelle. L'exigence était que chaque appel à mettre en file d'attente créerait une nouvelle file d'attente qui contenait l'ancienne file d'attente et le nouvel élément à la fin. Dequeue retournerait également une nouvelle file d'attente et l'élément retiré de la file d'attente en tant que paramètre de sortie.

La création d'un IEnumerator à partir de cette implémentation serait non destructive. Et laissez-moi vous dire que l'implémentation d'une file d'attente fonctionnelle qui fonctionne bien est beaucoup plus difficile que l'implémentation d'une pile fonctionnelle performante (la pile Push / Pop fonctionne à la fois sur la queue, car une file d'attente de file d'attente fonctionne sur la queue, la file d'attente fonctionne sur la tête).

Mon point étant ... il est trivial de créer un énumérateur de pile non destructif en implémentant votre propre mécanisme de pointeur (StackNode <T>) et en utilisant la sémantique fonctionnelle dans l'énumérateur.

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

Quelques choses à noter. Un appel à push ou pop avant l'instruction current = _head; completes vous donnerait une pile différente pour l'énumération que s'il n'y avait pas de multithreading (vous voudrez peut-être utiliser un ReaderWriterLock pour vous protéger contre cela). J'ai fait les champs dans StackNode en lecture seule mais bien sûr, si T est un objet mutable, vous pouvez changer ses valeurs. Si vous deviez créer un constructeur Stack qui a pris un StackNode comme paramètre (et définir la tête sur celui passé dans le nœud). Deux piles construites de cette manière n'auront pas d'impact l'une sur l'autre (à l'exception d'un T mutable comme je l'ai mentionné). Vous pouvez pousser et faire éclater tout ce que vous voulez dans une pile, l'autre ne changera pas.

Et que mon ami est la façon dont vous faites l'énumération non destructive d'une pile.


+1: Mes énumérateurs non destructifs de pile et de file d'attente sont implémentés de la même manière. Je pensais plus dans le sens des PQ / Heaps avec des comparateurs personnalisés.
Steven Evers

1
Je dirais que j'utilise la même approche ... je ne peux pas dire que j'ai déjà implémenté un PQ fonctionnel ... on dirait que cet article suggère d'utiliser des séquences ordonnées comme approximation non linéaire
Michael Brown

0

Pour tenter de simplifier les choses, .NET ne possède qu'une seule paire d'interfaces générique / non générique pour les choses qui sont énumérées en appelant GetEnumerator()puis en utilisant MoveNextet Currentsur l'objet reçu à partir de celles-ci, même s'il existe au moins quatre types d'objets. qui ne devrait prendre en charge que les méthodes suivantes:

  1. Des choses qui peuvent être énumérées au moins une fois, mais pas nécessairement plus que cela.
  2. Éléments qui peuvent être énumérés un nombre arbitraire de fois dans des contextes de thread libre, mais qui peuvent produire arbitrairement un contenu différent à chaque fois
  3. Choses qui peuvent être énumérées un nombre arbitraire de fois dans des contextes libres, mais qui garantissent que si le code qui les énumère à plusieurs reprises n'appelle aucune méthode de mutation, toutes les énumérations renverront le même contenu.
  4. Les choses qui peuvent être énumérées un nombre arbitraire de fois et qui sont garanties de renvoyer le même contenu à chaque fois qu'elles existent.

Toute instance qui satisfait à l'une des définitions les plus élevées satisfera également toutes les plus basses, mais le code qui nécessite un objet satisfaisant à l'une des définitions les plus élevées peut se casser s'il est donné l'une des plus basses.

Il semble que Microsoft ait décidé que les classes qui implémentent IEnumerable<T>devraient satisfaire la deuxième définition, mais ne sont pas tenues de satisfaire quelque chose de plus élevé. Sans doute, il n'y a pas beaucoup de raisons pour que quelque chose qui ne pourrait que répondre à la première définition devrait mettre en œuvre IEnumerable<T>plutôt que IEnumerator<T>; si les foreachboucles pouvaient accepter IEnumerator<T>, il serait logique que les choses ne puissent être énumérées qu'une seule fois pour implémenter simplement cette dernière interface. Malheureusement, les types qui implémentent uniquement IEnumerator<T>sont moins pratiques à utiliser en C # et VB.NET que les types qui ont une GetEnumeratorméthode.

Dans tous les cas, même s'il serait utile qu'il y ait différents types énumérables pour des choses qui pourraient donner des garanties différentes, et / ou une manière standard de demander à une instance qui implémente IEnumerable<T>quelles garanties elle peut faire, aucun type de ce type n'existe encore.


0

Je pense que ce serait une violation du principe de substitution de Liskov qui stipule,

les objets d'un programme doivent être remplaçables par des instances de leurs sous-types sans altérer l'exactitude de ce programme.

Si j'avais une méthode comme la suivante qui est appelée plus d'une fois dans mon programme,

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

Je devrais pouvoir échanger n'importe quel IEnumerable sans altérer l'exactitude du programme, mais l'utilisation de la file d'attente destructrice modifierait considérablement le comportement des programmes.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.