2010-02-20 19 views
11

este código:Bug en el método File.ReadLines (..) de .NET Framework 4.0

IEnumerable<string> lines = File.ReadLines("file path"); 
foreach (var line in lines) 
{ 
    Console.WriteLine(line); 
} 
foreach (var line in lines) 
{ 
    Console.WriteLine(line); 
} 

lanza una ObjectDisposedException : {"Cannot read from a closed TextReader."} si se ejecuta la segunda foreach. Parece que el objeto iterador devuelto desde File.ReadLines(..) no se puede enumerar más de una vez. Debe obtener un nuevo objeto iterador llamando al File.ReadLines(..) y luego usarlo para iterar.

Si sustituyo File.ReadLines(..) con mi versión (parámetros no se verifican, es sólo un ejemplo):

public static IEnumerable<string> MyReadLines(string path) 
{ 
    using (var stream = new TextReader(path)) 
    { 
     string line; 
     while ((line = stream.ReadLine()) != null) 
     { 
      yield return line; 
     } 
    } 
} 

es posible repetir más de una vez las líneas del archivo.

Una investigación que utiliza .Net Reflector mostró que la implementación de File.ReadLines(..) llama a un File.InternalReadLines(TextReader reader) privado que crea el iterador real. El lector pasó como un parámetro se utiliza en el método MoveNext() del iterador para obtener las líneas del archivo y se elimina cuando llegamos al final del archivo. Esto significa que una vez que MoveNext() devuelve falso no hay forma de iterar una segunda vez porque el lector está cerrado y debe obtener un nuevo lector creando un nuevo iterador con el método ReadLines(..).En mi versión, se crea un nuevo lector en el MoveNext() método cada vez que comenzamos una nueva iteración.

¿Es este el comportamiento esperado del método File.ReadLines(..)?

Me parece preocupante el hecho de que es necesario llamar al método cada vez antes de enumerar los resultados. También debería llamar al método cada vez antes de repetir los resultados de una consulta de Linq que utiliza el método.

+0

_ "¿Es este el comportamiento esperado del método File.ReadLines (..)?" _ Sí. Si ha consumido un 'StreamReader', será eliminado. No hay camino de ida y vuelta. Si lo necesita, debe usar 'File.ReadAllLines'. –

+0

En realidad, una solución simple como 'IEnumerable ReadLinesFixed (ruta de cadena) {foreach (línea var en File.ReadLines (ruta)) produce la línea de retorno; } 'funciona también. – Vlad

Respuesta

5

No creo que sea un error, y no creo que sea inusual; de hecho, eso es lo que esperaría de algo así como un lector de archivos de texto. IO es una operación costosa, por lo que en general desea hacer todo en una sola pasada.

+8

Sí, pero el lector se podría crear en la llamada IEnumerable.GetEnumerator, es decir, cuando comienza la enumeración, no cuando se crea IEnumerable. Estoy de acuerdo con Adrián, ese sería un comportamiento más predecible y más fácil de usar con los operadores LINQ que el nuevo método pretende soportar (y más en consonancia con esos operadores LINQ ya que son flojos). – itowlson

0

Si necesita acceder a las líneas dos veces siempre se pueden proteger ellos en un List<T>

using System.Linq; 

List<string> lines = File.ReadLines("file path").ToList(); 
foreach (var line in lines) 
{ 
    Console.WriteLine(line); 
} 
foreach (var line in lines) 
{ 
    Console.WriteLine(line); 
} 
+0

El problema es que esto requiere que .NET lea todo el lote en * a la vez *, lo que puede ser muy ineficiente para un archivo grande. El objetivo del método ReadLines era evitar la necesidad de esto (que, como Stephen señala, ya está siendo manejado adecuadamente por ReadAllLines). – itowlson

+1

No gano nada si guardo los resultados en una lista. También podría usar ReadAllLines(), que no es flojo y devuelve una matriz de cadenas. Si el archivo para leer es muy grande, esta operación llevaría mucho tiempo. Tengo que esperar que se devuelva toda la matriz (o lista) de cadenas antes de que pueda acceder a la matriz (o la lista). –

+0

@Adrian, si está analizando archivos de gran tamaño, evitaría esto. – bendewey

1

No es un error. Pero creo que puedes usar ReadAllLines() para hacer lo que quieras en su lugar. ReadAllLines crea una matriz de cadenas y extrae todas las líneas en la matriz, en lugar de simplemente un enumerador sobre una secuencia como lo hace ReadLines.

+0

Como mencioné antes, hay casos en los que preferiría no esperar a que se devuelva toda la matriz antes de poder usar los datos en la matriz. Normalmente, este es el caso cuando los archivos son grandes y termina con una matriz de 100 MB en la memoria. Puedo comenzar a enumerar las líneas antes de que se devuelva toda la colección. –

+1

Raramente he visto a nadie pelear obteniendo buenas respuestas a una buena pregunta tan difícil. Claramente, no es un error. La documentación explica el comportamiento y la explicación coincide con el comportamiento real. Hay dos métodos, uno permite la enumeración simple sin buffer sobre una secuencia de solo lectura. El otro almacena el contenido en una matriz para los casos en que necesita un búfer reutilizable. Los tipos de devolución coinciden con esta intención. La versión sin búfer devuelve IEnumerable. El búfer devuelve una matriz. Esto solo hace que la intención de los dos métodos sea bastante clara. –

+0

Con una matriz, no puede iniciar una enumeración antes de que la matriz esté completamente cargada. La matriz cambiará mientras la itera, lo que está explícitamente prohibido. Pareces sugerir que quieres tener una transmisión que puedas tratar como una matriz más adelante. Esta bien. Hay objetos como ese, especialmente en varias implementaciones de LINQ. Pero esto no es lo que * estos * métodos particulares hacen. Al igual que cualquier otra cosa, puede usar estos y otros métodos similares para hacer lo más complejo que desee. Solo escribe una clase que haga las cosas de esa manera. –

0

No sé si puede ser considerado como un error o no si es por diseño, pero sin duda puedo decir dos cosas ...

  1. Esto debe ser publicado en Connect, no Stackoverflow aunque' No vamos a cambiarlo antes de que se lance 4.0. Y eso generalmente significa que nunca lo arreglarán.
  2. El diseño del método ciertamente parece ser defectuoso.

Tiene razón al señalar que devolver un IEnumerable implica que debe ser reutilizable y no garantiza los mismos resultados si se repite dos veces. Si hubiera devuelto un IEnumerator en su lugar, entonces sería una historia diferente.

De todos modos, creo que es un buen hallazgo y creo que la API es mala para empezar.ReadAllLines y ReadAllText le brindan una forma cómoda y conveniente de obtener todo el archivo, pero si la persona que llama se preocupa lo suficiente por el rendimiento como para usar un enumerable perezoso, no deberían delegar tanta responsabilidad en un método de ayuda estático en primer lugar.

+0

IEnumerable no implica reutilización. Solo implica la capacidad de obtener un simple enumerador. Una gran cantidad de IEnumerables forward only no reutilizables están en el framework. Existen otras interfaces que se aplican a la mayoría de los objetos que son reutilizables o que proporcionan más que simples enumeraciones (IList, por ejemplo). –

+1

No estoy de acuerdo. Tuve cuidado de no decir "garantía" porque no es así. Pero ciertamente * implica * reutilización. Incluso el tipo de IEnumerator implica reutilización debido a su método de Restablecimiento.Sin embargo, esperaría que llamar a IEnumerable.GetEnumerator varias veces no arrojara o devolviera la misma instancia, ya que así es como se comporta prácticamente cada otro IEnumerable, incluidas las consultas LINQ. – Josh

0

Creo que está confundiendo un IQueryable con un IEnumerable. Sí, es cierto que IQueryable se puede tratar como IEnumerable, pero no son exactamente lo mismo. Una consulta de IQueryable cada vez que se usa, mientras que un IEnumerable no tiene dicha reutilización implícita.

Una consulta de Linq devuelve un IQueryable. ReadLines devuelve un IEnumerable.

Aquí hay una distinción sutil debido a la forma en que se crea un Enumerator. Un IQueryable crea un IEnumerator cuando se llama a GetEnumerator() en él (lo cual se hace automáticamente por foreach). ReadLines() crea el IEnumerator cuando se llama a la función ReadLines(). Como tal, cuando reutilizas un IQueryable, crea un nuevo IEnumerator cuando lo reutilizas, pero como ReadLines() crea el IEnumerator (y no un IQueryable), la única forma de obtener un nuevo IEnumerator es llamar a ReadLines() nuevamente .

En otras palabras, solo debería poder esperar volver a utilizar un IQueryable, no un IEnumerator.

EDIT:

En una reflexión más profunda (sin doble sentido) Creo que mi respuesta inicial fue un poco demasiado simplista. Si IEnumerable no era reutilizable, no se podía hacer algo como esto:

List<int> li = new List<int>() {1, 2, 3, 4}; 

IEnumerable<int> iei = li; 

foreach (var i in iei) { Console.WriteLine(i); } 
foreach (var i in iei) { Console.WriteLine(i); } 

Claramente, no se esperaría que la segunda foreach falle.

El problema, como suele ser el caso con este tipo de abstracciones, es que no todo encaja perfectamente. Por ejemplo, los flujos suelen ser unidireccionales, pero para el uso de la red debían adaptarse para funcionar bidireccionalmente.

En este caso, un dispositivo IEnumerable se concibió originalmente como una función reutilizable, pero desde entonces se ha adaptado para que sea tan genérico que la reutilización no sea una garantía o incluso esperada. Sea testigo de la explosión de varias bibliotecas que usan IEnumerables de maneras no reutilizables, como la biblioteca Jeffery Richters PowerThreading.

Simplemente no creo que podamos suponer que los documentos de IEnumerables son reutilizables en todos los casos.

+0

Ese podría ser el caso, pero la documentación en MSDN (http://msdn.microsoft.com/en-us/library/dd383503 (VS.100) .aspx) no especifica explícitamente que debe iterar solo una vez. Uno esperaría que se lanzara una excepción al intentar enumerar en el caso de tratar de modificar la colección que se está iterando. –

+0

@Adrian - ¿Desde cuándo hemos examinado la documentación de lo que no puede hacer? Normalmente lo miras por lo que * PUEDE * hacer. La documentación, por su propia naturaleza, a menudo es incompleta, por lo que usualmente tenemos suerte si nos dice todo lo que se puede hacer. Si incluye cosas que no pueden, eso tiende a ser más una anotación. –

0

No es un error. File.ReadLines() utiliza evaluación diferida y no es idempotent. Es por eso que no es seguro enumerarlo dos veces seguidas. Recuerde que IEnumerable representa una fuente de datos que se puede enumerar, no indica que es seguro enumerarla dos veces, aunque esto podría ser inesperado ya que la mayoría de la gente está acostumbrada a usar IEnumerable sobre colecciones idempotentes.

Desde el MSDN:

Los readlines (String, System) y ReadAllLines (String, Sistema) Métodos difieren de la siguiente manera: Cuando se utiliza readlines, puede empezar a enumerar la colección de cadenas antes de que se devuelva la colección completa ; Cuando se ReadAllLines uso, deberá esperar a que toda la matriz de cadenas ser devuelto antes de poder acceder al array.Therefore, cuando se está trabajando con archivos muy grandes, pueden readlines ser más eficiente.

Sus hallazgos a través del reflector son correctos y verifique este comportamiento. La implementación que proporcionó evita este comportamiento inesperado pero aún utiliza la evaluación perezosa.

+2

Este sería el primer y único ejemplo que he visto de una función IEnumerable.GetEnumerator que no se puede invocar más de una vez. –

+0

Hemos estado discutiendo esto intensamente sobre el proyecto morelinq y hemos decidido implementar a todos nuestros operadores como idempotentes. Los consumidores asumen que IEnumerables se pueden enumerar más de una vez. Nuevamente, en este caso no es un error, es una característica. –

+0

El hecho de que no pueda enumerar dos veces el IEnumerable devuelto por ReadLines (..) es solo un detalle de implementación. La excepción se arroja en el método MoveNext() del enumerador. Mi implementación utiliza el lector como una variable local y, por lo tanto, obtienes un nuevo TextReader cada vez que empiezas a enumerar. Claramente, el problema aquí es que necesita un nuevo TextReader una vez que haya terminado una enumeración. No veo ninguna razón por la cual un archivo no se iterará más de una vez. –

5

Sé que esto es viejo, pero en realidad me encontré con esto mientras trabajaba en algún código en una máquina con Windows 7. Contrariamente a lo que decía la gente aquí, esto realmente era un error. Ver this link.

Así que la solución más fácil es actualizar su .net framefork. Pensé que valía la pena actualizar ya que este fue el resultado de búsqueda superior.

Cuestiones relacionadas