Um iterador tem um contrato implícito não destrutivo?


11

Digamos que estou projetando uma estrutura de dados personalizada, como uma pilha ou uma fila (por exemplo - poderia ser outra coleção ordenada arbitrária que possua o equivalente lógico de pushe popmétodos - isto é, métodos destrutivos de acessador).

Se você estivesse implementando um iterador (no .NET, especificamente IEnumerable<T>) sobre essa coleção que aparecia em cada iteração, isso estaria violando IEnumerable<T>o contrato implícito?

Será que IEnumerable<T>têm esse contrato implícito?

por exemplo:

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

Respostas:


12

Eu acredito que um recenseador destrutivo viola o princípio da menor surpresa . Como um exemplo artificial, imagine uma biblioteca de objetos de negócios que ofereça funções de conveniência genéricas. Escrevo inocentemente uma função que atualiza uma coleção de objetos de negócios:

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

Outro desenvolvedor que não sabe nada sobre minha implementação ou sua implementação decide inocentemente usar as duas. É provável que eles fiquem surpresos com o resultado:

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

Mesmo que o desenvolvedor esteja ciente da enumeração destrutiva, ele pode não pensar nas repercussões ao entregar a coleção a uma função de caixa preta. Como autor UpdateStatus, provavelmente não considerarei enumerações destrutivas no meu design.

No entanto, é apenas um contrato implícito. As coleções do .NET, inclusive Stack<T>, impõem um contrato explícito com as suas InvalidOperationException- "A coleção foi modificada após a instanciação do enumerador". Você poderia argumentar que um verdadeiro profissional tem uma atitude de advertência em relação a qualquer código que não seja o seu. A surpresa de um enumerador destrutivo seria descoberta com testes mínimos.


1
+1 - para 'Princípio da menor surpresa', vou citar isso nas pessoas.

+1 Isso é basicamente o que eu estava pensando também, ser destrutivo pode interromper o comportamento esperado das extensões LINQ IEnumerable. Parece estranho iterar sobre esse tipo de coleção ordenada sem executar as operações normais / destrutivas.
Steven Evers

1

Um dos desafios de codificação mais interessantes dados a mim para uma entrevista foi criar uma fila funcional. O requisito era que cada chamada para enfileirar criaria uma nova fila que contivesse a fila antiga e o novo item na cauda. Retirar da fila também retornaria uma nova fila e o item retirado da fila como um parâmetro externo.

Criar um IEnumerator a partir desta implementação seria não destrutivo. E deixe-me dizer-lhe que implementar uma fila funcional com bom desempenho é muito mais difícil do que implementar uma pilha funcional de alto desempenho (a pilha Push / Pop funciona na cauda, ​​pois uma fila enfileira funciona na cauda, ​​a desenfileiramento funciona na cabeça).

Meu ponto de vista é ... é trivial criar um Enumerador de Pilha não destrutivo implementando seu próprio mecanismo de Ponteiro (StackNode <T>) e usando a semântica funcional no Enumerador.

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

Algumas coisas a serem observadas. Uma chamada para enviar ou enviar antes da instrução current = _head; concluídas forneceria uma pilha diferente para enumeração do que se não houvesse multithreading (convém usar um ReaderWriterLock para proteger contra isso). Eu fiz os campos no StackNode somente leitura, mas é claro que se T é um objeto mutável, você pode alterar seus valores. Se você criar um construtor Stack que utilizou um StackNode como parâmetro (e defina o cabeçalho como o passado no nó). Duas pilhas construídas dessa maneira não terão impacto uma na outra (com exceção de um T mutável, como mencionei). Você pode enviar por push e pop o quanto quiser em uma pilha, a outra não será alterada.

E que meu amigo é como você faz a enumeração não destrutiva de uma pilha.


+1: Meus enumeradores não destrutivos de pilha e fila são implementados de maneira semelhante. Eu estava pensando mais ao longo das linhas de PQs / Heaps com comparadores personalizados.
Steven Evers

1
Eu diria que usar a mesma abordagem ... não posso dizer que tenho implementado um PQ Funcional antes embora ... parece que este artigo sugere o uso de seqüências ordenadas como uma aproximação não-linear
Michael Brown

0

Na tentativa de manter as coisas "simples", o .NET possui apenas um par de interfaces genéricas / não genéricas para as coisas enumeradas chamando GetEnumerator()e depois usando MoveNexte Currentno objeto recebido delas, mesmo que haja pelo menos quatro tipos de objetos que deve suportar apenas os métodos:

  1. Coisas que podem ser enumeradas pelo menos uma vez, mas não necessariamente mais do que isso.
  2. Coisas que podem ser enumeradas um número arbitrário de vezes em contextos de encadeamento livre, mas podem arbitrariamente produzir conteúdo diferente a cada vez
  3. Coisas que podem ser enumeradas um número arbitrário de vezes em contextos de encadeamento livre, mas podem garantir que, se o código que as enumera repetidamente não chamar métodos de mutação, todas as enumerações retornarão o mesmo conteúdo.
  4. Coisas que podem ser enumeradas um número arbitrário de vezes e são garantidas para retornar o mesmo conteúdo todas as vezes que existirem.

Qualquer instância que satisfaça uma das definições de número mais alto também satisfará todas as definições mais baixas, mas o código que requer um objeto que satisfaça uma das definições mais altas pode ser interrompido se receber uma das definições mais baixas.

Parece que a Microsoft decidiu que as classes que implementam IEnumerable<T>devem satisfazer a segunda definição, mas não têm obrigação de satisfazer algo superior. Pode-se argumentar que não há muita razão para que algo que apenas atenda à primeira definição seja implementado IEnumerable<T>e não IEnumerator<T>; se os foreachloops pudessem aceitar IEnumerator<T>, faria sentido coisas que só podem ser enumeradas uma vez para simplesmente implementar a última interface. Infelizmente, tipos implementados apenas IEnumerator<T>são menos convenientes para usar em C # e VB.NET do que tipos que possuem um GetEnumeratormétodo.

De qualquer forma, mesmo que fosse útil se houvesse tipos diferentes de enumeráveis ​​para coisas que poderiam dar garantias diferentes e / ou uma maneira padrão de solicitar uma instância que implementasse IEnumerable<T>as garantias que ele pode dar, ainda não existem tais tipos.


0

Eu acho que isso seria uma violação do princípio de substituição de Liskov que afirma,

os objetos em um programa devem ser substituídos por instâncias de seus subtipos sem alterar a correção desse programa.

Se eu tivesse um método como o seguinte, chamado mais de uma vez no meu programa,

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

Eu deveria poder trocar qualquer IEnumerable sem alterar a correção do programa, mas o uso da fila destrutiva alteraria o comportamento do programa extensivamente.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.