Diferentemente dos C # IEnumerable
, em que um pipeline de execução pode ser executado quantas vezes quisermos, em Java, um fluxo pode ser 'iterado' apenas uma vez.
Qualquer chamada para uma operação do terminal fecha o fluxo, tornando-o inutilizável. Esse 'recurso' tira muito poder.
Imagino que o motivo disso não seja técnico. Quais foram as considerações de design por trás dessa estranha restrição?
Editar: para demonstrar o que estou falando, considere a seguinte implementação do Quick-Sort em C #:
IEnumerable<int> QuickSort(IEnumerable<int> ints)
{
if (!ints.Any()) {
return Enumerable.Empty<int>();
}
int pivot = ints.First();
IEnumerable<int> lt = ints.Where(i => i < pivot);
IEnumerable<int> gt = ints.Where(i => i > pivot);
return QuickSort(lt).Concat(new int[] { pivot }).Concat(QuickSort(gt));
}
Agora, com certeza, não estou defendendo que esta seja uma boa implementação de classificação rápida! No entanto, é um ótimo exemplo do poder expressivo da expressão lambda combinada com a operação de fluxo.
E isso não pode ser feito em Java! Não posso nem perguntar a um fluxo se ele está vazio sem torná-lo inutilizável.
fonte
IEnumerable
para os fluxos dejava.io.*
Respostas:
Tenho algumas lembranças do design inicial da API do Streams que podem lançar alguma luz sobre a lógica do design.
Em 2012, estávamos adicionando lambdas ao idioma e queríamos um conjunto de operações orientadas a coleções ou "dados em massa", programadas usando lambdas, que facilitassem o paralelismo. A idéia de encadear operações preguiçosamente juntas estava bem estabelecida nesse ponto. Também não queríamos que as operações intermediárias armazenassem resultados.
Os principais problemas que precisávamos decidir eram como eram os objetos na cadeia na API e como eles se conectavam às fontes de dados. As fontes eram frequentemente coleções, mas também queríamos suportar dados provenientes de um arquivo ou da rede ou dados gerados em tempo real, por exemplo, a partir de um gerador de números aleatórios.
Havia muitas influências do trabalho existente no design. Entre os mais influentes estavam a biblioteca Guava do Google e a biblioteca de coleções Scala. (Se alguém é surpreendido sobre a influência de goiaba, nota que Kevin Bourrillion , goiaba desenvolvedor líder, estava na JSR-335 Lambda . Grupo de peritos) em coleções Scala, encontramos essa conversa por Martin Odersky ser de particular interesse: futuro- Prova de coleções de Scala: de mutável a persistente a paralela . (Stanford EE380, 1º de junho de 2011)
Nosso design de protótipo na época era baseado em torno
Iterable
. As operações familiaresfilter
,map
etc., foram métodos de extensão (padrão) ativadosIterable
. Chamar um adicionou uma operação à cadeia e retornou outroIterable
. Uma operação terminalcount
chamariaiterator()
a cadeia até a fonte e as operações foram implementadas no Iterador de cada estágio.Como esses são iteráveis, você pode chamar o
iterator()
método mais de uma vez. O que deveria acontecer então?Se a fonte é uma coleção, isso geralmente funciona bem. As coleções são Iteráveis, e cada chamada
iterator()
produz uma instância Iterator distinta, independente de quaisquer outras instâncias ativas, e cada uma percorre a coleção independentemente. Ótimo.Agora, e se a fonte for única, como ler linhas de um arquivo? Talvez o primeiro iterador deva obter todos os valores, mas o segundo e os subsequentes devem estar vazios. Talvez os valores devam ser intercalados entre os iteradores. Ou talvez cada iterador deva obter os mesmos valores. Então, e se você tiver dois iteradores e um ficar mais à frente do outro? Alguém terá que armazenar em buffer os valores no segundo Iterador até que sejam lidos. Pior, e se você obtiver um Iterator e ler todos os valores, e somente então obter um segundo Iterator. De onde vêm os valores agora? Existe um requisito para que todos sejam armazenados em buffer, caso alguém queira um segundo iterador?
Claramente, permitir vários Iteradores sobre uma fonte de uma só vez levanta muitas questões. Não tínhamos boas respostas para eles. Queríamos um comportamento consistente e previsível para o que acontece se você ligar
iterator()
duas vezes. Isso nos levou a proibir várias travessias, tornando os oleodutos de uma só vez.Também observamos outros esbarrando nessas questões. No JDK, a maioria dos iteráveis são coleções ou objetos do tipo coleção, que permitem travessias múltiplas. Ele não está especificado em nenhum lugar, mas parecia haver uma expectativa não escrita de que os iteráveis permitem travessias múltiplas. Uma exceção notável é a interface NIO DirectoryStream . Sua especificação inclui este aviso interessante:
[negrito no original]
Isso parecia incomum e desagradável o suficiente para que não quiséssemos criar um monte de novos iteráveis que poderiam ser únicos. Isso nos afastou do uso do Iterable.
Naquela época, apareceu um artigo de Bruce Eckel que descrevia um certo problema que ele teve com Scala. Ele escreveu este código:
É bem direto. Ele analisa linhas de texto em
Registrant
objetos e as imprime duas vezes. Só que, na verdade, eles são impressos apenas uma vez. Acontece que ele pensou queregistrants
era uma coleção, quando na verdade é um iterador. A segunda chamada paraforeach
encontrar um iterador vazio, do qual todos os valores foram esgotados, portanto, ele não imprime nada.Esse tipo de experiência nos convenceu de que era muito importante ter resultados claramente previsíveis se tentássemos várias travessias. Ele também destacou a importância de distinguir entre estruturas preguiçosas do tipo pipeline e coleções reais que armazenam dados. Por sua vez, isso levou à separação das operações de pipeline lento na nova interface Stream e manteve apenas operações mutantes e ansiosas diretamente nas coleções. Brian Goetz explicou a justificativa para isso.
Que tal permitir travessia múltipla para pipelines baseados em coleção, mas não permitir para pipelines não baseados em coleção? É inconsistente, mas é sensato. Se você está lendo valores da rede, é claro que não pode atravessá-los novamente. Se você deseja atravessá-los várias vezes, é necessário atraí-los para uma coleção explicitamente.
Mas vamos explorar a possibilidade de atravessar vários pipelines baseados em coleções. Digamos que você fez isso:
(A
into
operação está agora escritacollect(toList())
.)Se a origem for uma coleção, a primeira
into()
chamada criará uma cadeia de iteradores de volta à origem, executará as operações do pipeline e enviará os resultados para o destino. A segunda chamada parainto()
criará outra cadeia de iteradores e executará as operações do pipeline novamente . Obviamente, isso não está errado, mas tem o efeito de executar todas as operações de filtro e mapa uma segunda vez para cada elemento. Eu acho que muitos programadores ficariam surpresos com esse comportamento.Como mencionei acima, estávamos conversando com os desenvolvedores do Guava. Uma das coisas legais que eles têm é um Idea Cemitério, onde descrevem recursos que decidiram não implementar juntamente com os motivos. A idéia de coleções preguiçosas parece bem legal, mas aqui está o que elas têm a dizer sobre isso. Considere uma
List.filter()
operação que retorna aList
:Para dar um exemplo específico, qual é o custo de
get(0)
ousize()
em uma lista? Para classes comumente usadas comoArrayList
, elas são O (1). Mas se você chamar um deles em uma lista filtrada lentamente, ele precisará executar o filtro na lista de suporte e, de repente, essas operações serão O (n). Pior, ele precisa percorrer a lista de suporte em todas as operações.Isso nos parecia preguiça demais . Uma coisa é configurar algumas operações e adiar a execução real até você "Ir". Outra é configurar as coisas de tal maneira que oculte uma quantidade potencialmente grande de recomputação.
Ao propor a proibição de fluxos não lineares ou "sem reutilização", Paul Sandoz descreveu as possíveis consequências de permitir que elas originassem "resultados inesperados ou confusos". Ele também mencionou que a execução paralela tornaria as coisas ainda mais complicadas. Por fim, acrescentaria que uma operação de pipeline com efeitos colaterais levaria a erros difíceis e obscuros se a operação fosse executada inesperadamente várias vezes, ou pelo menos um número diferente de vezes que o programador esperava. (Mas os programadores Java não escrevem expressões lambda com efeitos colaterais, fazem? Eles fazem?)
Portanto, essa é a lógica básica do design da API do Java 8 Streams que permite a passagem de um tiro e requer um pipeline estritamente linear (sem ramificação). Ele fornece um comportamento consistente em várias fontes de fluxo diferentes, separa claramente as operações preguiçosas das ansiosas e fornece um modelo de execução simples.
No que diz respeito a
IEnumerable
, estou longe de ser um especialista em C # e .NET, por isso gostaria de ser corrigido (suavemente) se tirar conclusões incorretas. Parece, no entanto, queIEnumerable
permite que a travessia múltipla se comporte de maneira diferente com fontes diferentes; e permite uma estrutura ramificada deIEnumerable
operações aninhadas , o que pode resultar em alguma recomputação significativa. Embora eu aprecie o fato de que sistemas diferentes fazem trocas diferentes, essas são duas características que procuramos evitar no design da API Java 8 Streams.O exemplo do quicksort dado pelo OP é interessante, intrigante e, lamento dizer, um pouco horrível. A chamada
QuickSort
recebeIEnumerable
e retorna umaIEnumerable
, portanto, nenhuma classificação é realmente feita até que a finalIEnumerable
seja percorrida. O que a chamada parece fazer, no entanto, é construir uma estrutura em árvoreIEnumerables
que reflita o particionamento que o quicksort faria, sem realmente fazê-lo. (Afinal, é uma computação preguiçosa.) Se a fonte tiver N elementos, a árvore terá N elementos de largura na sua largura mais ampla e níveis de lg (N) de profundidade.Parece-me - e mais uma vez, não sou especialista em C # ou .NET - que isso fará com que certas chamadas de aparência inócua, como a seleção de pivô
ints.First()
, sejam mais caras do que parecem. No primeiro nível, é claro, é O (1). Mas considere uma partição no fundo da árvore, na borda direita. Para calcular o primeiro elemento desta partição, toda a fonte deve ser atravessada, uma operação O (N). Porém, como as partições acima são preguiçosas, elas devem ser recalculadas, exigindo comparações de O (lg N). Portanto, selecionar o pivô seria uma operação O (N lg N), que é tão cara quanto uma classificação inteira.Mas na verdade não classificamos até atravessarmos o retornado
IEnumerable
. No algoritmo quicksort padrão, cada nível de particionamento dobra o número de partições. Cada partição tem apenas metade do tamanho, portanto, cada nível permanece com a complexidade O (N). A árvore de partições tem O (lg N) de altura, portanto o trabalho total é O (N lg N).Com a árvore de IEnumerables preguiçosos, na parte inferior da árvore há N partições. O cálculo de cada partição requer uma travessia de N elementos, cada um dos quais requer comparações de lg (N) na árvore. Para calcular todas as partições na parte inferior da árvore, é necessário comparações O (N ^ 2 lg N).
(Está certo? Mal posso acreditar nisso. Alguém por favor verifique isso por mim.)
De qualquer forma, é realmente interessante que
IEnumerable
possa ser usado dessa maneira para construir estruturas complicadas de computação. Mas se isso aumenta a complexidade computacional tanto quanto eu penso, parece que programar dessa maneira é algo que deve ser evitado, a menos que se seja extremamente cuidadoso.fonte
ints
: "Possível enumeração múltipla de IEnumerable". Usar o mesmoIEenumerable
mais de uma vez é suspeito e deve ser evitado. Eu também apontaria para esta pergunta (que eu respondi), que mostra algumas das advertências com a abordagem .Net (além do desempenho fraco): List <T> e IEnumerable diferençafundo
Embora a pergunta pareça simples, a resposta real requer alguns antecedentes para fazer sentido. Se você quiser pular para a conclusão, role para baixo ...
Escolha seu ponto de comparação - Funcionalidade básica
Usando conceitos básicos, o conceito do C #
IEnumerable
está mais estreitamente relacionado ao JavaIterable
, que é capaz de criar quantos Iteradores você desejar.IEnumerables
criarIEnumerators
. Do JavaIterable
criarIterators
A história de cada conceito é semelhante, em que tanto
IEnumerable
eIterable
têm uma motivação básica para permitir que 'for-each' estilo looping sobre os membros de coletas de dados. Isso é uma simplificação excessiva, pois ambos permitem mais do que apenas isso, e eles também chegaram a esse estágio por meio de diferentes progressões, mas é uma característica comum significativa, independentemente.Vamos comparar esse recurso: nas duas linguagens, se uma classe implementa o
IEnumerable
/Iterable
, essa classe deve implementar pelo menos um método único (para C #, éGetEnumerator
e para Java, éiterator()
). Em cada caso, a instância retornada disso (IEnumerator
/Iterator
) permite acessar os membros atuais e subseqüentes dos dados. Esse recurso é usado na sintaxe de cada idioma.Escolha seu ponto de comparação - Funcionalidade aprimorada
IEnumerable
em C # foi estendido para permitir vários outros recursos de idioma ( principalmente relacionados ao Linq ). Os recursos adicionados incluem seleções, projeções, agregações, etc. Essas extensões têm uma forte motivação do uso na teoria de conjuntos, semelhante aos conceitos SQL e Banco de Dados Relacional.O Java 8 também teve a funcionalidade adicionada para permitir um certo grau de programação funcional usando Streams e Lambdas. Observe que os fluxos do Java 8 não são motivados principalmente pela teoria dos conjuntos, mas pela programação funcional. Independentemente disso, existem muitos paralelos.
Então, este é o segundo ponto. Os aprimoramentos feitos no C # foram implementados como um aprimoramento do
IEnumerable
conceito. Em Java, no entanto, as melhorias feitas foram implementadas criando novos conceitos básicos de Lambdas e Streams, e também criando uma maneira relativamente trivial de converter deIterators
eIterables
para Streams, e vice-versa.Portanto, a comparação do IEnumerable com o conceito de Stream do Java está incompleta. Você precisa compará-lo com as APIs Streams e Collections combinadas em Java.
Em Java, o Streams não é o mesmo que Iterables ou Iterators
Os fluxos não são projetados para resolver problemas da mesma maneira que os iteradores:
Com um
Iterator
, você obtém um valor de dados, processa-o e, em seguida, obtém outro valor de dados.Com o Streams, você encadeia uma sequência de funções, alimenta um valor de entrada no fluxo e obtém o valor de saída da sequência combinada. Observe que, em termos de Java, cada função é encapsulada em uma única
Stream
instância. A API do Streams permite vincular uma sequência deStream
instâncias de maneira que encadeie uma sequência de expressões de transformação.Para concluir o
Stream
conceito, você precisa de uma fonte de dados para alimentar o fluxo e de uma função terminal que consome o fluxo.A maneira como você alimenta valores no fluxo pode ser de
Iterable
, mas aStream
sequência em si não éIterable
, é uma função composta.A
Stream
também se destina a ser preguiçoso, no sentido de que só funciona quando você solicita um valor.Observe estas premissas e recursos significativos do Streams:
Stream
em Java é um mecanismo de transformação, ele transforma um item de dados em um estado para outro.Comparação de C #
Quando você considera que um Java Stream é apenas parte de um sistema de fornecimento, fluxo e coleta, e que Streams e Iterators são frequentemente usados em conjunto com o Collections, não é de admirar que seja difícil se relacionar com os mesmos conceitos que são quase todos incorporados a um único
IEnumerable
conceito em C #.Partes do IEnumerable (e conceitos próximos) são aparentes em todos os conceitos de Java Iterator, Iterable, Lambda e Stream.
Existem pequenas coisas que os conceitos de Java podem fazer que são mais difíceis no IEnumerable e vice-versa.
Conclusão
A adição de Streams oferece mais opções para a solução de problemas, o que é justo classificar como 'aprimorando o poder', não 'reduzindo', 'retirando' ou 'restringindo-o'.
Por que o Java Streams é único?
Esta pergunta é equivocada, porque fluxos são sequências de funções, não dados. Dependendo da fonte de dados que alimenta o fluxo, é possível redefinir a fonte de dados e alimentar o mesmo fluxo ou outro fluxo.
Diferentemente do IEnumerable do C #, onde um pipeline de execução pode ser executado quantas vezes quisermos, em Java, um fluxo pode ser 'iterado' apenas uma vez.
Comparar um
IEnumerable
a umStream
é equivocado. O contexto que você está usando para dizerIEnumerable
pode ser executado quantas vezes quiser, é melhor comparado ao JavaIterables
, que pode ser iterado quantas vezes você desejar. Um JavaStream
representa um subconjunto doIEnumerable
conceito, e não o subconjunto que fornece dados e, portanto, não pode ser executado novamente.Qualquer chamada para uma operação do terminal fecha o fluxo, tornando-o inutilizável. Esse 'recurso' tira muito poder.
A primeira afirmação é verdadeira, em certo sentido. A declaração 'tira o poder' não é. Você ainda está comparando Streams it IEnumerables. A operação do terminal no fluxo é como uma cláusula 'break' em um loop for. Você está sempre livre para ter outro fluxo, se quiser e se puder fornecer novamente os dados necessários. Novamente, se você considerar o
IEnumerable
mais parecido com umIterable
, para esta declaração, o Java faz muito bem.Imagino que o motivo disso não seja técnico. Quais foram as considerações de design por trás dessa estranha restrição?
O motivo é técnico e pelo simples motivo de um Stream ser um subconjunto do que ele pensa que é. O subconjunto de fluxo não controla o fornecimento de dados, portanto, você deve redefinir o fornecimento, não o fluxo. Nesse contexto, não é tão estranho.
Exemplo do QuickSort
Seu exemplo do quicksort tem a assinatura:
Você está tratando a entrada
IEnumerable
como uma fonte de dados:Além disso, o valor de retorno
IEnumerable
também é , que é um fornecimento de dados e, como se trata de uma operação de Classificação, a ordem desse fornecimento é significativa. Se você considerar aIterable
classe Java a correspondência apropriada para isso, especificamente aList
especialização deIterable
, como List é um fornecimento de dados que possui uma ordem ou iteração garantida, o código Java equivalente ao seu código seria:Observe que há um erro (que eu reproduzi), em que a classificação não lida com valores duplicados normalmente, é uma classificação de 'valor exclusivo'.
Observe também como o código Java usa fonte de dados (
List
) e transmite conceitos em pontos diferentes, e que em C # essas duas 'personalidades' podem ser expressas apenasIEnumerable
. Além disso, embora eu tenha usadoList
como o tipo base, eu poderia ter usado o mais geralCollection
e, com uma pequena conversão de iterador para fluxo, eu poderia ter usado o ainda mais geralIterable
fonte
Stream
é um conceito de ponto no tempo, não uma 'operação de circuito' .... (cont.)f(x)
O stream encapsula a função, não encapsula os dados que fluem através deIEnumerable
também pode fornecer valores aleatórios, ser desvinculados e tornar-se ativo antes que os dados existam.IEnumerable<T>
expectativa de que ela represente uma coleção finita que pode ser repetida várias vezes. Algumas coisas que são iteráveis, mas não atendem a essas condições, são implementadasIEnumerable<T>
porque nenhuma outra interface padrão se encaixa na conta, mas os métodos que esperam coleções finitas que podem ser iteradas várias vezes são propensos a travar se houver coisas iteráveis que não respeitem essas condições. .quickSort
exemplo poderia ser muito mais simples se retornasse aStream
; economizaria duas.stream()
ligações e uma.collect(Collectors.toList())
ligação. Se você, em seguida, substituirCollections.singleton(pivot).stream()
comStream.of(pivot)
o código torna-se quase legível ...Stream
s são construídos em torno deSpliterator
s, que são objetos mutáveis e com estado. Eles não têm uma ação de "redefinição" e, de fato, exigir suporte a essa ação de retrocesso "tiraria muito poder". ComoRandom.ints()
deveria lidar com essa solicitação?Por outro lado, para
Stream
s que têm uma origem recuperável, é fácil construir um equivalenteStream
para ser usado novamente. Basta colocar as etapas feitas para transformar oStream
método em um reutilizável. Lembre-se de que repetir essas etapas não é uma operação cara, pois todas essas etapas são operações preguiçosas; o trabalho real começa com a operação do terminal e, dependendo da operação real do terminal, um código completamente diferente pode ser executado.Cabe a você, o criador de tal método, especificar o que chamar o método duas vezes implica: ele reproduz exatamente a mesma sequência, como fazem os fluxos criados para uma matriz ou coleção não modificada ou produz um fluxo com um semânticas semelhantes, mas elementos diferentes, como um fluxo de entradas aleatórias ou um fluxo de linhas de entrada do console etc.
A propósito, para evitar confusão, uma operação de terminal consome o
Stream
que é distinto de fechar oStream
que a chamadaclose()
no fluxo faz (o que é necessário para fluxos com recursos associados, como, por exemplo, produzidos porFiles.lines()
).Parece que muita confusão decorre da comparação equivocada de
IEnumerable
comStream
. UmIEnumerable
representa a capacidade de fornecer um realIEnumerator
, então é como umIterable
em Java. Por outro lado, aStream
é um tipo de iterador e comparável a um,IEnumerator
por isso é errado afirmar que esse tipo de dados pode ser usado várias vezes no .NET, o suporte paraIEnumerator.Reset
é opcional. Os exemplos discutidos aqui usam o fato de que umIEnumerable
pode ser usado para buscar novos seIEnumerator
funciona com os de JavaCollection
também; Você pode conseguir um novoStream
. Se os desenvolvedores Java decidissem adicionar asStream
operaçõesIterable
diretamente, com operações intermediárias retornando outraIterable
, era realmente comparável e poderia funcionar da mesma maneira.No entanto, os desenvolvedores decidiram contra e a decisão é discutida nesta questão . O ponto mais importante é a confusão sobre as operações ansiosas de cobrança e as preguiçosas operações de fluxo. Ao olhar para a API .NET, eu (sim, pessoalmente) considero justificada. Embora pareça razoável olhar
IEnumerable
sozinho, uma coleção específica terá muitos métodos para manipular a coleção diretamente e muitos métodos retornando um atrasoIEnumerable
, enquanto a natureza específica de um método nem sempre é intuitivamente reconhecível. O pior exemplo que encontrei (dentro de alguns minutos em que olhei) éList.Reverse()
cujo nome corresponde exatamente ao nome do herdado (esse é o terminal correto para métodos de extensão?),Enumerable.Reverse()
Apesar de ter um comportamento totalmente contraditório.Obviamente, essas são duas decisões distintas. O primeiro a
Stream
diferenciar um tipo deIterable
/Collection
e o segundo a tornarStream
um tipo de iterador único em vez de outro tipo de iterável. Mas essas decisões foram tomadas em conjunto e pode ser que a separação dessas duas decisões nunca tenha sido considerada. Não foi criado para ser comparável ao do .NET em mente.A decisão real do design da API foi adicionar um tipo aprimorado de iterador, o
Spliterator
.Spliterator
s podem ser fornecidos pelos antigosIterable
s (que é a maneira como eles foram adaptados) ou implementações inteiramente novas. Em seguida,Stream
foi adicionado como um front-end de alto nível ao nível bastante baixoSpliterator
s. É isso aí. Você pode discutir se um design diferente seria melhor, mas isso não é produtivo, não muda, dada a forma como eles são projetados agora.Há outro aspecto de implementação que você deve considerar.
Stream
s não são estruturas de dados imutáveis. Cada operação intermediária pode retornar uma novaStream
instância que encapsula a antiga, mas também pode manipular sua própria instância e retornar a si mesma (isso não impede a execução de ambos pela mesma operação). Exemplos comumente conhecidos são operações comoparallel
ouunordered
que não adicionam outra etapa, mas manipulam todo o pipeline). Ter uma estrutura de dados tão mutável e tentar reutilizar (ou pior ainda, usá-la várias vezes ao mesmo tempo) não funciona bem…Para ser completo, eis o exemplo do quicksort traduzido para a
Stream
API Java . Isso mostra que realmente não "tira muito poder".Pode ser usado como
Você pode escrever ainda mais compacto como
fonte
Stream
enquanto a redefinição da fonteSpliterator
seria implícita. E tenho certeza de que, se isso era possível, havia uma pergunta no SO como "Por que chamarcount()
duas vezes em umStream
dá resultados diferentes a cada vez", etc ...Stream
Até agora, todas as perguntas sobre SO sobre a natureza não reutilizável de s decorrem de uma tentativa de resolver um problema chamando as operações do terminal várias vezes (obviamente, caso contrário você não percebe), o que levou a uma solução silenciosamente interrompida se aStream
API permitir. com resultados diferentes em cada avaliação. Aqui está um bom exemplo .Eu acho que existem muito poucas diferenças entre os dois quando você olha de perto o suficiente.
No que diz respeito, um
IEnumerable
parece ser uma construção reutilizável:No entanto, o compilador está realmente trabalhando um pouco para nos ajudar; gera o seguinte código:
Cada vez que você realmente itera sobre o enumerável, o compilador cria um enumerador. O enumerador não é reutilizável; mais chamadas para
MoveNext
retornarão apenas false, e não há como redefini-lo para o início. Se você deseja repetir os números novamente, será necessário criar outra instância do enumerador.Para ilustrar melhor que o IEnumerable possui (pode ter) o mesmo 'recurso' que um Java Stream, considere um enumerável cuja origem dos números não seja uma coleção estática. Por exemplo, podemos criar um objeto enumerável que gera uma sequência de 5 números aleatórios:
Agora, temos um código muito semelhante ao enumerável baseado em matriz anterior, mas com uma segunda iteração sobre
numbers
:Na segunda vez que iteramos
numbers
, obteremos uma sequência diferente de números, que não é reutilizável no mesmo sentido. Ou, poderíamos ter escrito oRandomNumberStream
comando para lançar uma exceção se você tentar iterá-la várias vezes, tornando o enumerável realmente inutilizável (como um Java Stream).Além disso, o que sua classificação rápida baseada em enumerável significa quando aplicada a um
RandomNumberStream
?Conclusão
Portanto, a maior diferença é que o .NET permite reutilizar um
IEnumerable
, criando implicitamente um novoIEnumerator
em segundo plano sempre que for necessário acessar elementos na sequência.Esse comportamento implícito geralmente é útil (e "poderoso", como você declara), porque podemos repetidamente repetir uma coleção.
Mas, às vezes, esse comportamento implícito pode realmente causar problemas. Se sua fonte de dados não é estática ou é de alto custo de acesso (como um banco de dados ou site), muitas suposições
IEnumerable
precisam ser descartadas; reutilizar não é tão simplesfonte
É possível ignorar algumas das proteções "executar uma vez" na API Stream; por exemplo, podemos evitar
java.lang.IllegalStateException
exceções (com a mensagem "o fluxo já foi operado ou fechado") referenciando e reutilizando oSpliterator
(e não oStream
diretamente).Por exemplo, esse código será executado sem gerar uma exceção:
No entanto, a produção será limitada a
em vez de repetir a saída duas vezes. Isso ocorre porque o
ArraySpliterator
usado como aStream
fonte é estável e armazena sua posição atual. Quando repetimos issoStream
, começamos novamente no final.Temos várias opções para resolver esse desafio:
Poderíamos fazer uso de um
Stream
método de criação sem estado , comoStream#generate()
. Teríamos que gerenciar o estado externamente em nosso próprio código e redefinir entreStream
"replays":Outra solução (um pouco melhor, mas não perfeita) para isso é escrever nossa própria
ArraySpliterator
(ouStream
fonte similar ), que inclui alguma capacidade de redefinir o contador atual. Se o usássemos para gerar oStream
, poderíamos reproduzi-los com êxito.A melhor solução para esse problema (na minha opinião) é fazer uma nova cópia de qualquer estado
Spliterator
usado noStream
pipeline quando novos operadores forem chamados noStream
. Isso é mais complexo e envolvido na implementação, mas se você não se importa em usar bibliotecas de terceiros, o cyclops-react possui umaStream
implementação que faz exatamente isso. (Divulgação: Eu sou o desenvolvedor principal deste projeto.)Isso imprimirá
como esperado.
fonte