A diferencia de C # IEnumerable
, donde una tubería de ejecución se puede ejecutar tantas veces como queramos, en Java una secuencia se puede 'iterar' solo una vez.
Cualquier llamada a una operación de terminal cierra la secuencia y la deja inutilizable. Esta 'característica' le quita mucho poder.
Me imagino que la razón de esto no es técnica. ¿Cuáles fueron las consideraciones de diseño detrás de esta extraña restricción?
Editar: para demostrar de lo que estoy hablando, considere la siguiente implementación de Clasificación rápida en 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));
}
Ahora, para estar seguro, ¡no estoy abogando por que esta sea una buena implementación de tipo rápido! Sin embargo, es un gran ejemplo del poder expresivo de la expresión lambda combinada con la operación de flujo.
¡Y no se puede hacer en Java! Ni siquiera puedo preguntarle a un flujo si está vacío sin dejarlo inutilizable.
fuente
IEnumerable
de las corrientes dejava.io.*
Respuestas:
Tengo algunos recuerdos del diseño inicial de la API de Streams que podrían arrojar algo de luz sobre la lógica del diseño.
En 2012, estábamos agregando lambdas al lenguaje, y queríamos un conjunto de operaciones orientadas a colecciones o "datos masivos", programadas usando lambdas, que facilitaran el paralelismo. La idea de encadenar perezosamente las operaciones juntas estaba bien establecida en este punto. Tampoco queríamos que las operaciones intermedias almacenaran resultados.
Los principales problemas que necesitábamos decidir eran cómo se veían los objetos en la cadena en la API y cómo se conectaban a las fuentes de datos. Las fuentes a menudo eran colecciones, pero también queríamos admitir datos provenientes de un archivo o la red, o datos generados sobre la marcha, por ejemplo, de un generador de números aleatorios.
Hubo muchas influencias del trabajo existente en el diseño. Entre los más influyentes estaban la biblioteca de guayaba de Google y la biblioteca de colecciones Scala. (Si alguien está sorprendido por la influencia de Guava, tenga en cuenta que Kevin Bourrillion , desarrollador principal de Guava, estaba en el grupo de expertos JSR-335 Lambda ). En las colecciones de Scala, encontramos que esta charla de Martin Odersky es de particular interés: Future- Pruebas de colecciones Scala: de mutable a persistente a paralela . (Stanford EE380, 1 de junio de 2011)
Nuestro diseño de prototipo en ese momento se basaba en
Iterable
. Las operaciones familiaresfilter
,map
etc. fueron métodos de extensión (predeterminados)Iterable
. Llamar a uno agregó una operación a la cadena y devolvió otroIterable
. Una operación de terminal comocount
llamaríaiterator()
la cadena a la fuente, y las operaciones se implementaron dentro del iterador de cada etapa.Como estos son Iterables, puede llamar al
iterator()
método más de una vez. ¿Qué debería pasar entonces?Si la fuente es una colección, esto generalmente funciona bien. Las colecciones son Iterable, y cada llamada a
iterator()
produce una instancia Iterator distinta que es independiente de cualquier otra instancia activa, y cada una atraviesa la colección de forma independiente. Excelente.Ahora, ¿qué pasa si la fuente es de una sola vez, como leer líneas de un archivo? Quizás el primer iterador debería obtener todos los valores, pero el segundo y los siguientes deberían estar vacíos. Quizás los valores deberían estar entrelazados entre los iteradores. O tal vez cada iterador debería obtener los mismos valores. Entonces, ¿qué pasa si tienes dos iteradores y uno se adelanta al otro? Alguien tendrá que almacenar los valores en el segundo iterador hasta que se lean. Peor aún, qué pasa si obtienes un Iterador y lees todos los valores, y solo entonces obtienes un segundo Iterador. ¿De dónde vienen los valores ahora? ¿Existe algún requisito para que todos estén protegidos en caso de que alguien quiera un segundo iterador?
Claramente, permitir múltiples iteradores sobre una fuente de una sola vez plantea muchas preguntas. No teníamos buenas respuestas para ellos. Queríamos un comportamiento consistente y predecible para lo que sucede si llamas
iterator()
dos veces. Esto nos empujó a no permitir múltiples recorridos, haciendo que las tuberías fueran de una sola vez.También observamos que otros se toparon con estos problemas. En el JDK, la mayoría de los Iterables son colecciones u objetos similares a colecciones, que permiten un recorrido múltiple. No se especifica en ninguna parte, pero parece haber una expectativa no escrita de que los Iterables permiten un recorrido múltiple. Una excepción notable es la interfaz NIO DirectoryStream . Su especificación incluye esta interesante advertencia:
[negrita en original]
Esto parecía inusual y lo suficientemente desagradable como para que no quisiéramos crear un montón de nuevos Iterables que pudieran ser de una sola vez. Esto nos alejó del uso de Iterable.
Por esta época, apareció un artículo de Bruce Eckel que describía un problema que había tenido con Scala. Había escrito este código:
Es bastante sencillo. Analiza líneas de texto en
Registrant
objetos y las imprime dos veces. Excepto que en realidad solo los imprime una vez. Resulta que él pensó queregistrants
era una colección, cuando en realidad es un iterador. La segunda llamada aforeach
encuentra un iterador vacío, desde el cual se han agotado todos los valores, por lo que no imprime nada.Este tipo de experiencia nos convenció de que era muy importante tener resultados claramente predecibles si se intenta un recorrido múltiple. También destacó la importancia de distinguir entre estructuras perezosas tipo tubería de colecciones reales que almacenan datos. Esto, a su vez, condujo a la separación de las operaciones de tubería diferida a la nueva interfaz de Stream y mantuvo solo operaciones ansiosas y mutantes directamente en Colecciones. Brian Goetz ha explicado la justificación de eso.
¿Qué pasa con permitir el recorrido múltiple para tuberías basadas en colecciones pero no permitirlo para tuberías no basadas en colecciones? Es inconsistente, pero es sensato. Si está leyendo valores de la red, por supuesto, no puede atravesarlos nuevamente. Si desea atravesarlos varias veces, debe incluirlos explícitamente en una colección.
Pero exploremos permitiendo múltiples recorridos desde tuberías basadas en colecciones. Digamos que hiciste esto:
(La
into
operación ahora se deletreacollect(toList())
).Si el origen es una colección, la primera
into()
llamada creará una cadena de iteradores de regreso al origen, ejecutará las operaciones de canalización y enviará los resultados al destino. La segunda llamada ainto()
creará otra cadena de iteradores y ejecutará nuevamente las operaciones de canalización . Esto obviamente no está mal, pero tiene el efecto de realizar todas las operaciones de filtro y mapa por segunda vez para cada elemento. Creo que muchos programadores se habrían sorprendido por este comportamiento.Como mencioné anteriormente, habíamos estado hablando con los desarrolladores de Guava. Una de las cosas interesantes que tienen es un cementerio de ideas donde describen características que decidieron no implementar junto con los motivos. La idea de colecciones perezosas suena genial, pero esto es lo que tienen que decir al respecto. Considere una
List.filter()
operación que devuelve unList
:Para poner un ejemplo concreto, ¿cuál es el costo de
get(0)
osize()
en una lista? Para clases de uso común comoArrayList
, son O (1). Pero si llama a uno de estos en una lista filtrada perezosamente, tiene que ejecutar el filtro sobre la lista de respaldo, y de repente estas operaciones son O (n). Peor aún, tiene que recorrer la lista de respaldo en cada operación.Esto nos pareció demasiada pereza. Una cosa es configurar algunas operaciones y diferir la ejecución real hasta que "vaya". Otra es configurar las cosas de tal manera que oculte una cantidad potencialmente grande de recálculo.
Al proponer no permitir flujos no lineales o de "no reutilización", Paul Sandoz describió las posibles consecuencias de permitirlos como resultado de "resultados inesperados o confusos". También mencionó que la ejecución paralela haría las cosas aún más complicadas. Finalmente, agregaría que una operación de canalización con efectos secundarios conduciría a errores difíciles y oscuros si la operación se ejecutara inesperadamente varias veces, o al menos un número diferente de veces de lo que esperaba el programador. (Pero los programadores de Java no escriben expresiones lambda con efectos secundarios, ¿verdad? ¿HACEN?)
Esa es la razón básica para el diseño de la API de Java 8 Streams que permite un recorrido de una sola vez y que requiere una tubería estrictamente lineal (sin ramificación). Proporciona un comportamiento consistente a través de múltiples fuentes de flujo diferentes, claramente separa las operaciones perezosas de las ansiosas, y proporciona un modelo de ejecución directo.
Con respecto a esto
IEnumerable
, estoy lejos de ser un experto en C # y .NET, por lo que agradecería que me corrijan (suavemente) si saco conclusiones incorrectas. Sin embargo, parece queIEnumerable
permite que el recorrido múltiple se comporte de manera diferente con diferentes fuentes; y permite una estructura de ramificación deIEnumerable
operaciones anidadas , lo que puede resultar en una recalculación significativa. Si bien aprecio que los diferentes sistemas hacen diferentes compensaciones, estas son dos características que buscamos evitar en el diseño de la API de Java 8 Streams.El ejemplo de clasificación rápida dado por el OP es interesante, desconcertante, y lamento decirlo, algo horrible. La llamada
QuickSort
toma unIEnumerable
y devuelve unIEnumerable
, por lo que no se realiza ninguna clasificación hasta queIEnumerable
se atraviesa el final . Sin embargo, lo que parece hacer la llamada es construir una estructura de árbolIEnumerables
que refleje la partición que haría Quicksort, sin hacerlo realmente. (Esto es un cálculo lento, después de todo). Si la fuente tiene N elementos, el árbol tendrá N elementos de ancho en su parte más ancha, y tendrá niveles de lg (N) de profundidad.Me parece, y una vez más, no soy un experto en C # o .NET, que esto hará que ciertas llamadas de aspecto inocuo, como la selección de pivote a través
ints.First()
, sean más caras de lo que parecen. En el primer nivel, por supuesto, es O (1). Pero considere una partición profunda en el árbol, en el borde derecho. Para calcular el primer elemento de esta partición, se debe atravesar toda la fuente, una operación O (N). Pero dado que las particiones anteriores son perezosas, deben volverse a calcular, lo que requiere comparaciones de O (lg N). Por lo tanto, seleccionar el pivote sería una operación O (N lg N), que es tan costosa como una clase completa.Pero en realidad no clasificamos hasta que atravesamos el retorno
IEnumerable
. En el algoritmo de clasificación rápida estándar, cada nivel de partición duplica el número de particiones. Cada partición es solo la mitad del tamaño, por lo que cada nivel permanece en la complejidad O (N). El árbol de particiones es O (lg N) alto, por lo que el trabajo total es O (N lg N).Con el árbol de IEnumerables perezosos, en la parte inferior del árbol hay N particiones. Calcular cada partición requiere un recorrido de N elementos, cada uno de los cuales requiere comparaciones lg (N) en el árbol. Para calcular todas las particiones en la parte inferior del árbol, entonces, se requieren comparaciones O (N ^ 2 lg N).
(¿Es correcto? Apenas puedo creer esto. Alguien por favor verifique esto por mí).
En cualquier caso, es realmente genial que
IEnumerable
se pueda usar de esta manera para construir estructuras complicadas de cómputo. Pero si aumenta la complejidad computacional tanto como creo que lo hace, parecería que la programación de esta manera es algo que debería evitarse a menos que uno sea extremadamente cuidadoso.fuente
ints
: "Posible enumeración múltiple de IEnumerable". Usar el mismoIEenumerable
más de una vez es sospechoso y debe evitarse. También señalaría esta pregunta (que he respondido), que muestra algunas de las advertencias con el enfoque .Net (además del bajo rendimiento): Lista <T> y I Diferencia numerableAntecedentes
Si bien la pregunta parece simple, la respuesta real requiere algunos antecedentes para tener sentido. Si desea pasar a la conclusión, desplácese hacia abajo ...
Elija su punto de comparación: funcionalidad básica
Usando conceptos básicos, el concepto de C #
IEnumerable
está más estrechamente relacionado con el de JavaIterable
, que puede crear tantos iteradores como desee.IEnumerables
crearIEnumerators
.Iterable
Crear JavaIterators
La historia de cada concepto es similar, en la que tanto
IEnumerable
yIterable
tener una motivación básica para permitir 'for-each' estilo de bucle a través de los miembros de las colecciones de datos. Eso es una simplificación excesiva, ya que ambos permiten más que eso, y también llegaron a esa etapa a través de diferentes progresiones, pero es una característica común importante independientemente.Comparemos esa característica: en ambos lenguajes, si una clase implementa el
IEnumerable
/Iterable
, entonces esa clase debe implementar al menos un método único (para C #, esGetEnumerator
y para Java esiterator()
). En cada caso, la instancia devuelta desde ese (IEnumerator
/Iterator
) le permite acceder a los miembros actuales y posteriores de los datos. Esta característica se utiliza en la sintaxis de cada idioma.Elija su punto de comparación: funcionalidad mejorada
IEnumerable
en C # se ha ampliado para permitir una serie de otras características del lenguaje ( principalmente relacionadas con Linq ). Las características agregadas incluyen selecciones, proyecciones, agregaciones, etc. Estas extensiones tienen una fuerte motivación para su uso en la teoría de conjuntos, similar a los conceptos de SQL y Base de datos relacional.Java 8 también ha agregado funcionalidades para permitir un grado de programación funcional usando Streams y Lambdas. Tenga en cuenta que las secuencias de Java 8 no están motivadas principalmente por la teoría de conjuntos, sino por la programación funcional. En cualquier caso, hay muchos paralelos.
Entonces, este es el segundo punto. Las mejoras realizadas en C # se implementaron como una mejora del
IEnumerable
concepto. Sin embargo, en Java, las mejoras realizadas se implementaron creando nuevos conceptos básicos de Lambdas y Streams, y luego también creando una forma relativamente trivial de convertir desdeIterators
yIterables
hacia Streams, y viceversa.Entonces, comparar IEnumerable con el concepto Stream de Java está incompleto. Debe compararlo con las API de Streams y Colecciones combinadas en Java.
En Java, los flujos no son lo mismo que Iterables o Iterator
Las transmisiones no están diseñadas para resolver problemas de la misma manera que los iteradores:
Con un
Iterator
, obtiene un valor de datos, lo procesa y luego obtiene otro valor de datos.Con Streams, encadena una secuencia de funciones juntas, luego alimenta un valor de entrada a la secuencia y obtiene el valor de salida de la secuencia combinada. Tenga en cuenta que, en términos de Java, cada función se encapsula en una sola
Stream
instancia. La API de Streams le permite vincular una secuencia deStream
instancias de una manera que encadena una secuencia de expresiones de transformación.Para completar el
Stream
concepto, necesita una fuente de datos para alimentar el flujo y una función de terminal que consume el flujo.De hecho, la forma en que introduce valores en la secuencia puede ser de un
Iterable
, pero laStream
secuencia en sí no es unIterable
, es una función compuesta.A
Stream
también está destinado a ser vago, en el sentido de que solo funciona cuando solicita un valor.Tenga en cuenta estos supuestos y características importantes de Streams:
Stream
en Java es un motor de transformación, transforma un elemento de datos en un estado, en otro estado.Comparación de C #
Cuando considera que un Java Stream es solo una parte de un sistema de suministro, transmisión y recolección, y que los Streams e Iteradores a menudo se usan junto con Colecciones, entonces no es de extrañar que sea difícil relacionarse con los mismos conceptos que son Casi todos integrados en un solo
IEnumerable
concepto en C #.Partes de IEnumerable (y conceptos relacionados cercanos) son evidentes en todos los conceptos de Java Iterator, Iterable, Lambda y Stream.
Hay pequeñas cosas que los conceptos de Java pueden hacer que son más difíciles en IEnumerable, y viceversa.
Conclusión
Agregar Streams le brinda más opciones para resolver problemas, lo que es justo clasificar como 'potenciación', no 'reducción', 'eliminación' o 'restricción'.
¿Por qué los flujos de Java son únicos?
Esta pregunta es errónea, porque las secuencias son secuencias de funciones, no datos. Dependiendo de la fuente de datos que alimenta la secuencia, puede restablecer la fuente de datos y alimentar la misma o diferente secuencia.
A diferencia de IEnumerable de C #, donde una tubería de ejecución se puede ejecutar tantas veces como queramos, en Java una secuencia se puede 'iterar' solo una vez.
Comparar un
IEnumerable
a unStream
está equivocado. El contexto que está utilizando para decir queIEnumerable
se puede ejecutar tantas veces como desee, se compara mejor con JavaIterables
, que se puede repetir tantas veces como desee. Un JavaStream
representa un subconjunto delIEnumerable
concepto, y no el subconjunto que proporciona datos y, por lo tanto, no se puede "volver a ejecutar".Cualquier llamada a una operación de terminal cierra la secuencia y la deja inutilizable. Esta 'característica' le quita mucho poder.
La primera afirmación es cierta, en cierto sentido. La declaración 'quita el poder' no lo es. Todavía está comparando Streams it IEnumerables. La operación del terminal en el flujo es como una cláusula de 'interrupción' en un bucle for. Siempre puede tener otra secuencia, si lo desea, y si puede volver a suministrar los datos que necesita. Nuevamente, si considera
IEnumerable
que es más como unIterable
, para esta declaración, Java lo hace bien.Me imagino que la razón de esto no es técnica. ¿Cuáles fueron las consideraciones de diseño detrás de esta extraña restricción?
La razón es técnica, y por la sencilla razón de que un Stream es un subconjunto de lo que cree que es. El subconjunto de flujo no controla el suministro de datos, por lo que debe restablecer el suministro, no el flujo. En ese contexto, no es tan extraño.
Ejemplo de QuickSort
Su ejemplo de clasificación rápida tiene la firma:
Está tratando la entrada
IEnumerable
como una fuente de datos:Además, el valor de retorno
IEnumerable
también lo es, que es un suministro de datos, y dado que esta es una operación de Clasificación, el orden de ese suministro es significativo. Si considera que laIterable
clase Java es la coincidencia adecuada para esto, específicamente laList
especialización deIterable
, dado que List es un suministro de datos que tiene un orden o iteración garantizados, entonces el código Java equivalente a su código sería:Tenga en cuenta que hay un error (que he reproducido), ya que el tipo no maneja los valores duplicados con gracia, es un tipo de "valor único".
También tenga en cuenta cómo el código Java usa la fuente de datos (
List
), y transmite conceptos en diferentes puntos, y que en C # esas dos 'personalidades' se pueden expresar de manera justaIEnumerable
. Además, aunque lo he usadoList
como tipo base, podría haber usado el más generalCollection
, y con una pequeña conversión de iterador a Stream, podría haber usado el aún más general.Iterable
fuente
Stream
es un concepto de punto en el tiempo, no una 'operación de bucle' ... (cont.)f(x)
El flujo encapsula la función, no encapsula los datos que fluyen a través de élIEnumerable
También puede proporcionar valores aleatorios, ser independiente y activarse antes de que existan los datos.IEnumerable<T>
esperan que represente una colección finita que se puede repetir varias veces. Algunas cosas que son iterables pero que no cumplen esas condiciones se implementanIEnumerable<T>
porque ninguna otra interfaz estándar cumple con los requisitos , pero los métodos que esperan colecciones finitas que se pueden repetir varias veces son propensos a fallar si se les dan cosas iterables que no cumplen con esas condiciones .quickSort
ejemplo podría ser mucho más simple si devuelve unStream
; ahorraría dos.stream()
llamadas y una.collect(Collectors.toList())
llamada. Si luego lo reemplazaCollections.singleton(pivot).stream()
conStream.of(pivot)
el código se vuelve casi legible ...Stream
s se construyen alrededor deSpliterator
s que son objetos mutables con estado. No tienen una acción de "reinicio" y, de hecho, la necesidad de apoyar dicha acción de rebobinado "les quitaría mucho poder". ¿Cómo seRandom.ints()
supone que manejaría tal solicitud?Por otro lado, para
Stream
s que tienen un origen rastreable, es fácil construir un equivalenteStream
para ser usado nuevamente. Simplemente ponga los pasos realizados para construir elStream
en un método reutilizable. Tenga en cuenta que repetir estos pasos no es una operación costosa ya que todos estos pasos son operaciones perezosas; el trabajo real comienza con la operación del terminal y, dependiendo de la operación real del terminal, se puede ejecutar un código completamente diferente.Depende de usted, el escritor de dicho método, especificar qué implica llamar dos veces al método: ¿reproduce exactamente la misma secuencia, como lo hacen las secuencias creadas para una matriz o colección no modificada, o produce una secuencia con un semántica similar pero elementos diferentes como un flujo de entradas aleatorias o un flujo de líneas de entrada de consola, etc.
Por cierto, para evitar confusiones, una operación de terminal consume la
Stream
que es distinta de cierre de laStream
como llamarclose()
en la corriente no (que se requiere para los flujos que tiene asociado recursos como, por ejemplo producido porFiles.lines()
).Parece que mucha confusión proviene de la comparación equivocada de
IEnumerable
conStream
. UnIEnumerable
representa la capacidad de proporcionar un realIEnumerator
, por lo que es como unIterable
en Java. Por el contrario, aStream
es un tipo de iterador y comparable a un,IEnumerator
por lo que es incorrecto afirmar que este tipo de datos se puede usar varias veces en .NET, el soporte paraIEnumerator.Reset
es opcional. Los ejemplos discutidos aquí usan más bien el hecho de que unIEnumerable
puede usarse para obtener nuevos mensajes de correoIEnumerator
electrónico y eso también funciona con los mensajes de JavaCollection
; Puedes conseguir uno nuevoStream
. Si los desarrolladores de Java decidieron agregar lasStream
operacionesIterable
directamente, las operaciones intermedias devuelven otraIterable
, fue realmente comparable y podría funcionar de la misma manera.Sin embargo, los desarrolladores decidieron no hacerlo y la decisión se discute en esta pregunta . El punto más importante es la confusión sobre las ansiosas operaciones de Colección y las operaciones de Stream diferidas. Al mirar la API .NET, (sí, personalmente) me parece justificada. Si bien parece razonable analizarlo
IEnumerable
solo, una Colección particular tendrá muchos métodos para manipular la Colección directamente y muchos métodos que devolverán un vagoIEnumerable
, mientras que la naturaleza particular de un método no siempre es intuitivamente reconocible. El peor ejemplo que encontré (en los pocos minutos que lo vi) esList.Reverse()
cuyo nombre coincide exactamente con el nombre del heredado (¿es este el término correcto para los métodos de extensión?) YEnumerable.Reverse()
tiene un comportamiento totalmente contradictorio.Por supuesto, estas son dos decisiones distintas. El primero para hacer
Stream
un tipo distinto deIterable
/Collection
y el segundo para hacerStream
un tipo de iterador de tiempo en lugar de otro tipo de iterador. Pero estas decisiones se tomaron juntas y podría darse el caso de que nunca se consideró separar estas dos decisiones. No fue creado teniendo en cuenta que es comparable a .NET.La decisión real del diseño de la API fue agregar un tipo mejorado de iterador, el
Spliterator
.Spliterator
Los s pueden ser proporcionados por los viejosIterable
s (que es la forma en que se actualizaron) o implementaciones completamente nuevas. Luego,Stream
se agregó como un front-end de alto nivel a los niveles bastante bajosSpliterator
. Eso es. Puede discutir si un diseño diferente sería mejor, pero eso no es productivo, no cambiará, dada la forma en que están diseñados ahora.Hay otro aspecto de implementación que debes considerar.
Stream
Los s no son estructuras de datos inmutables. Cada operación intermedia puede devolver una nuevaStream
instancia encapsulando la anterior, pero también puede manipular su propia instancia en su lugar y devolverse a sí misma (eso no impide hacer ni siquiera las dos cosas para la misma operación). Ejemplos comúnmente conocidos son operaciones comoparallel
ounordered
que no agregan otro paso sino que manipulan toda la tubería). Tener una estructura de datos tan mutable e intentos de reutilización (o peor aún, usarlos varias veces al mismo tiempo) no funciona bien ...Para completar, aquí está su ejemplo de clasificación rápida traducido a la
Stream
API de Java . Muestra que en realidad no "quita mucho poder".Se puede usar como
Puedes escribirlo aún más compacto como
fuente
Stream
mientras que el reinicio de las fuentesSpliterator
estaría implícito. Y estoy bastante seguro de que si eso fuera posible, hubo preguntas en SO como "¿Por qué llamarcount()
dos veces a unStream
da resultados diferentes cada vez", etc ...Stream
s hasta ahora se deriva de un intento de resolver un problema llamando a operaciones de terminal varias veces (obviamente, de lo contrario no se da cuenta) lo que condujo a una solución silenciosa si laStream
API lo permitía con diferentes resultados en cada evaluación. Aquí hay un buen ejemplo .Creo que hay muy pocas diferencias entre los dos cuando miras lo suficientemente de cerca.
A primera vista,
IEnumerable
parece ser una construcción reutilizable:Sin embargo, el compilador realmente está haciendo un poco de trabajo para ayudarnos; genera el siguiente código:
Cada vez que usted realmente itera sobre el enumerable, el compilador crea un enumerador. El enumerador no es reutilizable; nuevas llamadas a
MoveNext
simplemente devolverán false, y no hay forma de restablecerlo al principio. Si desea repetir los números nuevamente, deberá crear otra instancia de enumerador.Para ilustrar mejor que IEnumerable tiene (puede tener) la misma 'característica' que Java Stream, considere un enumerable cuya fuente de números no sea una colección estática. Por ejemplo, podemos crear un objeto enumerable que genere una secuencia de 5 números aleatorios:
Ahora tenemos un código muy similar al enumerable basado en matriz anterior, pero con una segunda iteración sobre
numbers
:La segunda vez que iteramos
numbers
, obtendremos una secuencia de números diferente, que no es reutilizable en el mismo sentido. O bien, podríamos haber escrito elRandomNumberStream
para lanzar una excepción si intenta iterar sobre él varias veces, haciendo que el enumerable sea realmente inutilizable (como un Java Stream).Además, ¿qué significa su ordenación rápida basada en enumerable cuando se aplica a un
RandomNumberStream
?Conclusión
Por lo tanto, la mayor diferencia es que .NET le permite reutilizar una
IEnumerable
al crear implícitamente una nuevaIEnumerator
en segundo plano siempre que necesite acceder a elementos en la secuencia.Este comportamiento implícito es a menudo útil (y 'poderoso' como usted dice), porque podemos iterar repetidamente sobre una colección.
Pero a veces, este comportamiento implícito puede causar problemas. Si su fuente de datos no es estática, o su acceso es costoso (como una base de datos o sitio web), entonces
IEnumerable
se deben descartar muchas suposiciones sobre ; reutilizar no es tan sencillofuente
Es posible omitir algunas de las protecciones de "ejecutar una vez" en la API de Stream; por ejemplo, podemos evitar
java.lang.IllegalStateException
excepciones (con el mensaje "el flujo ya se ha operado o cerrado") haciendo referencia y reutilizando elSpliterator
(en lugar delStream
directamente).Por ejemplo, este código se ejecutará sin lanzar una excepción:
Sin embargo, la salida se limitará a
en lugar de repetir la salida dos veces. Esto se debe a que el
ArraySpliterator
utilizado comoStream
fuente tiene estado y almacena su posición actual. Cuando reproducimos estoStream
, comenzamos de nuevo al final.Tenemos varias opciones para resolver este desafío:
Podríamos hacer uso de un
Stream
método de creación sin estado comoStream#generate()
. Tendríamos que gestionar el estado externamente en nuestro propio código y restablecer entreStream
"repeticiones":Otra solución (ligeramente mejor pero no perfecta) para esto es escribir nuestra propia
ArraySpliterator
(oStream
fuente similar ) que incluya cierta capacidad para restablecer el contador actual. Si lo usáramos para generar elStream
, podríamos reproducirlo con éxito.La mejor solución a este problema (en mi opinión) es hacer una nueva copia de cualquier estado con estado
Spliterator
utilizado en laStream
tubería cuando se invocan nuevos operadores en elStream
. Esto es más complejo e implica implementarlo, pero si no le importa usar bibliotecas de terceros, cyclops-react tiene unaStream
implementación que hace exactamente esto. (Divulgación: soy el desarrollador principal de este proyecto).Esto imprimirá
como se esperaba.
fuente