Detectando IEnumerable "Máquinas de Estado"

17

Acabei de ler um artigo interessante chamado Ficando muito fofo com retorno de rendimento em c #

Isso me fez pensar qual é a melhor maneira de detectar se um IEnumerable é uma coleção enumerável real ou se é uma máquina de estado gerada com a palavra-chave yield.

Por exemplo, você pode modificar o DoubleXValue (do artigo) para algo como:

private void DoubleXValue(IEnumerable<Point> points)
{
    if(points is List<Point>)
      foreach (var point in points)
        point.X *= 2;
    else throw YouCantDoThatException();
}

Pergunta 1) Existe uma maneira melhor de fazer isso?

Pergunta 2) É algo com que eu deveria me preocupar ao criar uma API?

ConditionRacer
fonte
1
Você deve usar uma interface em vez de um concreto e também convertê-la mais abaixo, ICollection<T>seria uma escolha melhor, pois nem todas as coleções são List<T>. Por exemplo, matrizes são Point[]implementadas, IList<T>mas não são List<T>.
quer
3
Bem-vindo ao problema com tipos mutáveis. Ienumerable deve implicitamente ser um tipo de dados imutável, portanto, implementações mutantes são uma violação do LSP. Este artigo é uma explicação perfeita dos perigos dos efeitos colaterais; portanto, escreva seus tipos de C # para que sejam o mais livre de efeitos colaterais possível e você nunca precisará se preocupar com isso.
Jimmy Hoffa
1
@JimmyHoffa IEnumerableé imutável no sentido de que você não pode modificar uma coleção (adicionar / remover) enquanto a enumera, no entanto, se os objetos na lista são mutáveis, é perfeitamente razoável modificá-los enquanto enumera a lista.
quer
@TrevorPilley Eu discordo. Se se espera que a coleção seja imutável, a expectativa de um consumidor é que os membros não sejam mutados por atores externos, o que seria chamado de efeito colateral e viola a expectativa de imutabilidade. Viola POLA e implicitamente LSP.
Jimmy Hoffa
Que tal usar ToList? msdn.microsoft.com/en-us/library/bb342261.aspx Ele não detecta se foi gerado pelo rendimento, mas o torna irrelevante.
luiscubal

Respostas:

22

Sua pergunta, como eu a entendo, parece basear-se em uma premissa incorreta. Deixe-me ver se consigo reconstruir o raciocínio:

  • O artigo vinculado ao descreve como seqüências geradas automaticamente exibem um comportamento "preguiçoso" e mostra como isso pode levar a um resultado contra-intuitivo.
  • Portanto, posso detectar se uma determinada instância do IEnumerable exibirá esse comportamento preguiçoso, verificando se ele é gerado automaticamente.
  • Como faço isso?

O problema é que a segunda premissa é falsa. Mesmo se você pudesse detectar se um determinado IEnumerable foi ou não o resultado de uma transformação de bloco de iterador (e sim, existem maneiras de fazer isso), isso não ajudaria porque a suposição está errada. Vamos ilustrar o porquê.

class M { public int P { get; set; } }
class C
{
  public static IEnumerable<M> S1()
  {
    for (int i = 0; i < 3; ++i) 
      yield return new M { P = i };
  }

  private static M[] ems = new M[] 
  { new M { P = 0 }, new M { P = 1 }, new M { P = 2 } };
  public static IEnumerable<M> S2()
  {
    for (int i = 0; i < 3; ++i)
      yield return ems[i];
  }

  public static IEnumerable<M> S3()
  {
    return new M[] 
    { new M { P = 0 }, new M { P = 1 }, new M { P = 2 } };
  }

  private class X : IEnumerable<M>
  {
    public IEnumerator<X> GetEnumerator()
    {
      return new XEnum();
    }
    // Omitted: non generic version
    private class XEnum : IEnumerator<X>
    {
      int i = 0;
      M current;
      public bool MoveNext()
      {
        current = new M() { P = i; }
        i += 1;
        return true;
      }
      public M Current { get { return current; } }
      // Omitted: other stuff.
    }
  }

  public static IEnumerable<M> S4()
  {
    return new X();
  }

  public static void Add100(IEnumerable<M> items)
  {
    foreach(M item in items) item.P += 100;
  }
}

Tudo bem, temos quatro métodos. S1 e S2 são sequências geradas automaticamente; S3 e S4 são sequências geradas manualmente. Agora suponha que tenhamos:

var items = C.Sn(); // S1, S2, S3, S4
S.Add100(items);
Console.WriteLine(items.First().P);

O resultado para S1 e S4 será 0; toda vez que você enumera a sequência, você obtém uma nova referência a um M criado. O resultado para S2 e S3 será 100; toda vez que você enumera a sequência, você obtém a mesma referência a M da última vez. Se o código de sequência é gerado automaticamente ou não, é ortogonal à questão de saber se os objetos enumerados têm identidade referencial ou não. Essas duas propriedades - geração automática e identidade referencial - na verdade não têm nada a ver uma com a outra. O artigo ao qual você vinculou os confunde um pouco.

A menos que um provedor de sequência esteja documentado como sempre oferecendo objetos com identidade referencial , não é aconselhável supor que ele o faz.

Eric Lippert
fonte
2
Todos nós sabemos que você terá a resposta correta aqui, isso é dado; mas se você puder colocar um pouco mais de definição no termo "identidade referencial", isso pode ser bom, estou lendo como "imutável", mas é porque eu confundo imutável em C # com o novo objeto retornado a cada tipo de valor ou obtenção, que eu acho é a mesma coisa a que você está se referindo. Embora a imutabilidade concedida possa ser obtida de várias outras maneiras, é a única maneira racional em C # (que eu saiba, é possível que você conheça maneiras pelas quais não conheço).
Jimmy Hoffa
2
@ JimmyHoffa: Duas referências a objetos xey têm "identidade referencial" se Object.ReferenceEquals (x, y) retornar true.
precisa saber é o seguinte
Sempre bom receber uma resposta diretamente da fonte. Obrigado Eric.
ConditionRacer
10

Eu acho que o conceito principal é que você deve tratar qualquer IEnumerable<T>coleção como imutável : você não deve modificar os objetos, deve criar uma nova coleção com novos objetos:

private IEnumerable<Point> DoubleXValue(IEnumerable<Point> points)
{
    foreach (var point in points)
        yield return new Point(point.X * 2, point.Y);
}

(Ou escreva o mesmo de forma mais sucinta usando o LINQ Select().)

Se você realmente deseja modificar os itens da coleção, não use IEnumerable<T>, use IList<T>(ou possivelmente IReadOnlyList<T>se você não quiser modificar a coleção propriamente dita, apenas itens nela e você está no .Net 4.5):

private void DoubleXValue(IList<Point> points)
{
    foreach (var point in points)
        point.X *= 2;
}

Dessa forma, se alguém tentar usar seu método IEnumerable<T>, ele falhará no momento da compilação.

svick
fonte
1
A chave real é como você disse, retorne um novo número com as modificações quando desejar alterar alguma coisa. Inumerável como um tipo é perfeitamente viável e não tem motivo para ser alterado, é apenas a técnica para usá-lo precisa ser entendida como você disse. +1 por mencionar como trabalhar com tipos imutáveis.
Jimmy Hoffa
1
Um pouco relacionado - você também deve tratar todos os IEnumerable como se isso fosse feito por meio de execução adiada. O que pode ser verdade no momento em que você recupera sua referência IEnumerable pode não ser verdade quando você realmente enumera por meio dela. Por exemplo, retornar uma instrução LINQ dentro de um bloqueio pode resultar em alguns resultados interessantes e inesperados.
precisa saber é o seguinte
@JimmyHoffa: Eu concordo. Vou um passo além e tento evitar a iteração mais de uma IEnumerablevez. A necessidade de fazer isso mais de uma vez pode ser evitada chamando ToList()e iterando sobre a lista, embora no caso específico mostrado pelo OP prefira a solução da svick de ter o DoubleXValue também retorne um IEnumerable. Claro, em código real eu provavelmente usaria LINQpara gerar esse tipo de IEnumerable. No entanto, a escolha de svick yieldfaz sentido no contexto da pergunta do OP.
Brian
Brian @ Sim, eu não queria mudar muito o código para fazer o meu ponto. Em código real, eu teria usado Select(). E sobre várias iterações de IEnumerable: ReSharper é útil com isso, porque pode avisá-lo quando você faz isso.
svick
5

Pessoalmente, acho que o problema destacado nesse artigo decorre do uso excessivo da IEnumerableinterface por muitos desenvolvedores de C # atualmente. Parece ter se tornado o tipo padrão quando você está exigindo uma coleção de objetos - mas há muitas informações que IEnumerablesimplesmente não informam ... crucialmente: se foi ou não reificado *.

Se você achar que precisa detectar se um IEnumerableé realmente um IEnumerableou outra coisa, a abstração vazou e você provavelmente está em uma situação em que seu tipo de parâmetro é um pouco solto. Se você precisar de informações extras que por IEnumerablesi só não fornecem, torne esse requisito explícito escolhendo uma das outras interfaces como ICollectionou IList.

Edite para os que recusam. Volte e leia a pergunta com atenção. O OP está pedindo uma maneira de garantir que as IEnumerableinstâncias sejam materializadas antes de serem passadas para o método API. Minha resposta aborda essa pergunta. Há uma questão mais ampla sobre a maneira correta de usar IEnumerable, e certamente as expectativas do código colado pelo OP baseiam-se em um mal-entendido dessa questão mais ampla, mas o OP não perguntou como usá-lo corretamente IEnumerableou como trabalhar com tipos imutáveis . Não é justo me rebaixar por não responder a uma pergunta que não foi feita.

* Eu uso o termo reify, outros usam stabilizeou materialize. Eu não acho que uma convenção comum tenha sido adotada pela comunidade ainda.

MattDavey
fonte
2
Um problema com ICollectione IListé que eles são mutáveis ​​(ou pelo menos parecem assim). IReadOnlyCollectione IReadOnlyListdo .Net 4.5 resolvem alguns desses problemas.
svick
Por favor, explique o voto negativo?
precisa saber é o seguinte
1
Eu discordo que você deve alterar os tipos que está usando. Inumerável é um ótimo tipo e muitas vezes pode ter o benefício da imutabilidade. Acho que o verdadeiro problema é que as pessoas não entendem como trabalhar com tipos imutáveis ​​em c #. Não é de surpreender, é uma linguagem extremamente estável, portanto é um novo conceito para muitos desenvolvedores de C #.
Jimmy Hoffa
Apenas IEnumerable permite adiada execução, assim não permitindo-o como um parâmetro remove um recurso inteiro de C #
Jimmy Hoffa
2
Eu votei para baixo porque acho errado. Eu acho que isso está no espírito da SE, para garantir que as pessoas não coloquem informações incorretas, cabe a todos nós votar negativamente essas respostas. Talvez eu esteja errado, é totalmente possível que eu esteja errado e você esteja certa. Cabe à comunidade em geral decidir votando. Desculpe, você está levando para o lado pessoal. Se houver algum consolo, recebi muitas respostas negativas votadas que escrevi e apaguei sumariamente porque aceito que a comunidade acha que está errado, então provavelmente não estou certo.
Jimmy Hoffa