2010-02-20 21 views
11

Questo codice:errore nel metodo file.readlines (..) di .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); 
} 

genera ObjectDisposedException : {"Cannot read from a closed TextReader."} se il secondo foreach viene eseguita. Sembra che l'oggetto iteratore restituito da File.ReadLines(..) non possa essere enumerato più di una volta. È necessario ottenere un nuovo oggetto iteratore chiamando File.ReadLines(..) e quindi utilizzarlo per iterare.

Se sostituisco File.ReadLines(..) con la mia versione (parametri non sono verificate, è solo un esempio):

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

è possibile scorrere più di una volta le righe del file.

Un'indagine che utilizza .Net Reflector ha mostrato che l'implementazione dello File.ReadLines(..) chiama un privato File.InternalReadLines(TextReader reader) che crea l'iteratore effettivo. Il lettore ha passato come parametro viene utilizzato nel metodo MoveNext() dell'iteratore per ottenere le linee del file e viene eliminato quando raggiungiamo la fine del file. Ciò significa che una volta che MoveNext() restituisce false, non c'è modo di ripetere una seconda volta perché il lettore è chiuso e devi ottenere un nuovo lettore creando un nuovo iteratore con il metodo ReadLines(..).Nella mia versione viene creato un nuovo lettore nello MoveNext() metodo ogni volta che iniziamo una nuova iterazione.

È questo il comportamento previsto del metodo File.ReadLines(..)?

Trovo problematico il fatto che sia necessario chiamare il metodo ogni volta prima di enumerare i risultati. Dovresti anche chiamare il metodo ogni volta prima di iterare i risultati di una query Linq che utilizza il metodo.

+0

_ "È questo il comportamento previsto del metodo File.ReadLines (..)?" _ Sì. Se hai consumato un 'StreamReader', sarà eliminato. Non c'è modo di andare avanti e indietro. Se ti serve devi usare 'File.ReadAllLines'. –

+0

In realtà, una soluzione semplice come 'IEnumerable ReadLinesFixed (percorso stringa) {foreach (riga var in File.ReadLines (percorso)) restituisce una riga di ritorno; } 'funziona anche. – Vlad

risposta

5

Non penso che sia un bug, e non penso sia insolito - in effetti è quello che mi aspetterei per qualcosa come un lettore di file di testo. L'IO è un'operazione costosa, quindi in generale si vuole fare tutto in un unico passaggio.

+8

Sì, ma il lettore potrebbe essere creato nella chiamata IEnumerable.GetEnumerator, ad esempio quando inizia l'enumerazione, non quando viene creato IEnumerable. Sono d'accordo con Adrian, sarebbe un comportamento più prevedibile e più facile da usare con gli operatori LINQ che il nuovo metodo è destinato a supportare (e più coerente con quegli operatori LINQ poiché sono pigri). – itowlson

0

Se avete bisogno di accedere alle linee due volte si può sempre tampone in 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

Il problema è che questo richiede che .NET legga tutto il lotto * in una sola volta *, il che potrebbe essere molto inefficiente per un file di grandi dimensioni. L'intero punto del metodo ReadLines era di evitare la necessità di questo (che, come sottolinea Stephen, è già adeguatamente gestito da ReadAllLines). – itowlson

+1

Non guadagno nulla se memorizzo i risultati in un elenco. Potrei anche usare ReadAllLines() che non è pigro e restituisce una serie di stringhe. Se il file da leggere è molto grande, questa operazione richiederebbe molto tempo. Devo aspettare che venga restituito l'intero array (o elenco) di stringhe prima di poter accedere all'array (o all'elenco). –

+0

@Adrian, se stai analizzando file di grandi dimensioni, eviterei questo. – bendewey

1

E non è un bug. Ma credo che tu possa usare ReadAllLines() per fare invece ciò che vuoi. ReadAllLines crea un array di stringhe e inserisce tutte le linee nell'array, invece di un semplice enumeratore su un flusso come fa ReadLines.

+0

Come accennato in precedenza, ci sono casi in cui preferirei non attendere la restituzione dell'intero array prima di poter utilizzare i dati nell'array. In genere questo è il caso in cui i file sono grandi e si finisce con una matrice da 100 MB nella memoria. Posso iniziare a enumerare le righe prima che venga restituita l'intera collezione. –

+1

Raramente ho visto qualcuno lottare per ottenere delle buone risposte a una buona domanda così difficile. Chiaramente non è un bug. La documentazione spiega il comportamento e la spiegazione corrisponde al comportamento effettivo. Esistono due metodi, uno consente la semplice enumerazione senza buffer su un flusso di sola lettura. L'altro memorizza i contenuti in un array per i casi in cui è necessario un buffer riutilizzabile. I tipi di ritorno corrispondono a questo intento. Il unbuffered restituisce IEnumerable. Il buffer restituisce un array. Questo da solo rende abbastanza chiaro l'intento dei due diversi metodi. –

+0

Con una matrice, non è possibile avviare un'enumerazione prima che l'array sia completamente caricato. L'array cambierà mentre lo stai iterando, il che è esplicitamente disabilitato. Sembra che tu stia suggerendo che vuoi avere un flusso che puoi trattare come un array in un secondo momento. Va bene. Ci sono oggetti del genere, specialmente in varie implementazioni LINQ. Ma questo non è quello che * questi * metodi particolari fanno. Come qualsiasi cosa, puoi usare questi e altri metodi simili per fare la cosa più complessa che desideri. Scrivi semplicemente una classe che fa le cose in questo modo. –

0

Non so se può essere considerato un bug o se non è in base alla progettazione, ma posso certamente dire due cose ...

  1. Questo dovrebbe essere pubblicato su Connect, non StackOverflow anche se' non lo cambierò prima che venga rilasciato 4.0. E questo di solito significa che non lo risolveranno mai.
  2. Il design del metodo sembra certamente imperfetto.

Si ha ragione nel notare che restituire un oggetto IEnumerable implica che dovrebbe essere riutilizzabile e non garantisce gli stessi risultati se iterato due volte. Se avesse restituito un IEnumerator invece sarebbe una storia diversa.

Quindi, in ogni caso, penso che sia una buona scoperta e penso che l'API sia pessima per cominciare.ReadAllLines e ReadAllText offrono un modo comodo e conveniente per ottenere l'intero file, ma se il chiamante si preoccupa abbastanza delle prestazioni per utilizzare un enumerabile pigro, non dovrebbero delegare così tanta responsabilità a un metodo di supporto statico in primo luogo.

+0

IEnumerable non implica la riusabilità. Implica solo la possibilità di ottenere un semplice enumeratore. Un sacco di forward non riutilizzabili solo IEnumerables sono nel framework. Ci sono altre interfacce che si applicano alla maggior parte degli oggetti che sono riutilizzabili o forniscono più di semplici enumerazioni (IList per esempio). –

+1

Non sono d'accordo. Sono stato attento a non dire "garanzia" perché non lo è. Ma certamente * implica * riusabilità. Anche il tipo IEnumerator implica la riusabilità a causa del suo metodo Reset.Tuttavia, mi aspetto che chiamare più volte IEnumerable.GetEnumerator non debba generare o restituire la stessa istanza in cui si comporta virtualmente ogni altro oggetto IEnumerable, comprese le query LINQ. – Josh

0

Credo che si confonda un IQueryable con un oggetto IEnumerable. Sì, è vero che IQueryable può essere considerato come un oggetto IEnumerable, ma non sono esattamente la stessa cosa. Una query IQueryable ogni volta che viene utilizzata, mentre un oggetto IEnumerable non ha alcun riutilizzo implicito.

Una query di Linq restituisce un IQueryable. ReadLines restituisce un oggetto IEnumerable.

C'è una sottile distinzione qui a causa del modo in cui un Enumeratore viene creato. Un IQueryable crea un IEnumerator quando si chiama GetEnumerator() su di esso (operazione eseguita automaticamente da foreach). ReadLines() crea l'IEnumerator quando viene chiamata la funzione ReadLines(). Pertanto, quando riutilizzate un IQueryable, crea un nuovo IEnumerator quando lo riutilizzate, ma dal momento che ReadLines() crea l'IEnumerator (e non un IQueryable), l'unico modo per ottenere un nuovo IEnumerator è di chiamare nuovamente ReadLines() .

In altre parole, si dovrebbe essere in grado di riutilizzare un IQueryable, non un IEnumerator.

EDIT:

Su ulteriore riflessione (no pun intended) Credo che la mia risposta iniziale era un po 'troppo semplicistica. Se IEnumerable non era riutilizzabile, non si poteva fare qualcosa di simile:

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

Chiaramente, non ci si aspetterebbe il secondo foreach al sicuro.

Il problema, come spesso accade con questo tipo di astrazioni, è che non tutto si adatta perfettamente. Ad esempio, gli stream sono in genere a senso unico, ma per l'utilizzo in rete devono essere adattati per funzionare in modo bidirezionale.

In questo caso, un oggetto IEnumerable era originariamente concepito come una funzionalità riutilizzabile, ma da allora è stato adattato per essere così generico che la riusabilità non è una garanzia o addirittura dovrebbe essere prevista. Prova l'esplosione di varie librerie che usano IEnumerables in modi non riutilizzabili, come la libreria PowerThreading di Jeffery Richters.

Semplicemente non penso che possiamo supporre che IEnumerables sia sempre più riutilizzabile.

+0

Potrebbe essere il caso, ma la documentazione su MSDN (http://msdn.microsoft.com/en-us/library/dd383503 (VS.100) .aspx) non specifica esplicitamente che è necessario eseguire l'iterazione una sola volta. Ci si aspetterebbe che venisse generata un'eccezione durante il tentativo di enumerazione nel caso si tentasse di modificare la raccolta che viene iterata. –

+0

@Adrian - Da quando guardiamo la documentazione per ciò che non puoi fare? Di solito lo guardi per quello che * PUO * fare. La documentazione, per sua natura, è spesso incompleta, quindi siamo solitamente fortunati se ci dice tutto ciò che si può fare. Se include cose che non possono, questo tende ad essere più di un'annotazione. –

0

Non è un bug. File.ReadLines() utilizza la valutazione lazy e non è idempotent. Ecco perché non è sicuro enumerarlo due volte di seguito. Ricordare che uno IEnumerable rappresenta un'origine dati che può essere enumerata, non afferma che è sicuro essere enumerato due volte, sebbene ciò potrebbe essere inaspettato poiché la maggior parte delle persone è abituata a utilizzare IEnumerable su raccolte idempotent.

Dal MSDN:

I readlines (String, System) e ReadAllLines (String, sistema) Metodi differiscono nel modo seguente: Quando si utilizza readlines, è possibile avviare l'enumerazione la raccolta delle stringhe prima che venga restituita l'intera collezione ; quando si uso ReadAllLines, è necessario attendere l'intero array di stringhe restituita prima di poter accedere al array.Therefore, quando si lavora con file di grandi dimensioni, possono readlines essere più efficiente.

I risultati ottenuti tramite riflettore sono corretti e verificano questo comportamento. L'implementazione fornita evita questo comportamento imprevisto, ma fa comunque un uso della valutazione lazy.

+2

Questo sarebbe il primo e unico esempio che abbia mai visto di una funzione IEnumerable.GetEnumerator che non può essere chiamata più di una volta. –

+0

Abbiamo discusso intensamente di questo sul progetto morelinq e abbiamo deciso di implementare tutti i nostri operatori come identi- cienti. I consumatori naturalmente asume IEnumerables possono essere enumerati più di una volta. Ancora una volta, in questo caso non è un bug, è una funzionalità. –

+0

Il fatto che non è possibile enumerare il doppio dell'IEnumerable restituito da ReadLines (..) è solo un dettaglio di implementazione. L'eccezione viene generata nel metodo MoveNext() dell'enumeratore. La mia implementazione usa il lettore come una variabile locale e quindi ottieni un nuovo TextReader ogni volta che inizi a enumerare. Chiaramente il problema qui è che hai bisogno di un nuovo TextReader una volta terminata un'enumerazione. Non vedo alcun motivo per cui un file non verrebbe ripetuto più volte. –

5

So che questo è vecchio, ma in realtà mi sono imbattuto in questo mentre lavoravo su un codice su una macchina Windows 7. Contrariamente a ciò che le persone stavano dicendo qui, questo in realtà era un errore. Vedi this link.

Quindi la soluzione più semplice è aggiornare il tuo .net framefork. Ho pensato che valesse la pena di aggiornarlo poiché questo era il risultato di ricerca più alto.

Problemi correlati