2010-10-10 26 views
9

J'ai besoin de lire un flux deux fois, du début à la fin.Pourquoi éliminer StreamReader rend un flux illisible?

Mais le code suivant déclenche une exception ObjectDisposedException: Cannot access a closed file.

string fileToReadPath = @"<path here>"; 
using (FileStream fs = new FileStream(fileToReadPath, FileMode.Open)) 
{ 
    using (StreamReader reader = new StreamReader(fs)) 
    { 
     string text = reader.ReadToEnd(); 
     Console.WriteLine(text); 
    } 

    fs.Seek(0, SeekOrigin.Begin); // ObjectDisposedException thrown. 

    using (StreamReader reader = new StreamReader(fs)) 
    { 
     string text = reader.ReadToEnd(); 
     Console.WriteLine(text); 
    } 
} 

Pourquoi cela se produit-il? Qu'est-ce qui est vraiment disposé? Et pourquoi la manipulation StreamReader affecte le flux associé de cette manière? N'est-il pas logique de s'attendre à ce qu'un flux de recherche puisse être lu plusieurs fois, y compris par plusieurs StreamReader s?

+1

Envisagez également d'utiliser System.IO.File.ReadAllText() dans des situations comme celles-ci. C'est plus simple. –

+0

@ Dave Markle: vous avez raison. Je l'ai mis comme un court exemple. En fait, dans le code réel, les flux que je traite peuvent être très gros, donc le premier lecteur les lit ligne par ligne, ensuite, le flux est copié par octet dans un autre flux. –

Répondre

12

Cela se produit car le StreamReader prend en charge la «propriété» du flux. En d'autres termes, il se rend responsable de la fermeture du flux source. Dès que votre programme appelle Dispose ou Close (en laissant la portée de l'instruction using dans votre cas), il dispose également le flux source. Appelez le fs.Dispose() dans votre cas. Ainsi, le flux de fichier est mort après avoir quitté le premier bloc using. C'est un comportement cohérent, toutes les classes de flux dans .NET qui enveloppent un autre flux se comportent de cette façon.

Il existe un constructeur pour StreamReader qui permet de dire qu'il ne possède pas possède le flux source. Il n'est cependant pas accessible depuis un programme .NET, le constructeur est interne.

Dans ce cas particulier, vous pouvez résoudre le problème en n'utilisant pas le using -statement pour le StreamReader. C'est cependant un détail d'implémentation assez poilu. Il y a sûrement une meilleure solution à votre disposition mais le code est trop synthétique pour en proposer un vrai.

7

Le but de Dispose() est de nettoyer les ressources lorsque vous avez terminé avec le flux. La raison pour laquelle le lecteur influe sur le flux est que le lecteur ne fait que filtrer le flux, et donc éliminer le lecteur n'a de sens que dans le contexte de l'enchaînement de l'appel au flux source.

Pour corriger votre code, il suffit d'utiliser un lecteur tout le temps:

using (FileStream fs = new FileStream(fileToReadPath, FileMode.Open)) 
using (StreamReader reader = new StreamReader(fs)) 
{ 
    string text = reader.ReadToEnd(); 
    Console.WriteLine(text); 

    fs.Seek(0, SeekOrigin.Begin); // ObjectDisposedException not thrown now 

    text = reader.ReadToEnd(); 
    Console.WriteLine(text); 
} 

modifié pour tenir compte des commentaires ci-dessous:

Dans la plupart des cas, vous n'avez pas besoin d'accéder au flux sous-jacent vous faites dans votre code (fs.Seek). Dans ces cas, le fait que StreamReader enchaîne son appel au flux sous-jacent vous permet d'économiser sur le code en n'utilisant pas une instruction usings pour le flux. Par exemple, le code ressemblerait à ceci:

using (StreamReader reader = new StreamReader(new FileStream(fileToReadPath, FileMode.Open))) 
{ 
    ... 
} 
+0

En fait, je demande pourquoi Dispose() sur le * lecteur * affecte le * flux *. Je ne comprends pas ta réponse. Cela signifie-t-il que les lecteurs de Dispose ont seulement pour but de nettoyer les données du flux? Donc pourquoi même 'StreamReader' implémente' IDisposable', s'il ne fait rien * d'utile * (puisque disposer le flux lui-même fera dans tous les cas tout le travail)? –

+1

@MainMa, le but de Dispose() est * toujours * de s'assurer que toutes les ressources associées au 'IDisposable 'donné sont nettoyées. Puisque le lecteur et le flux représentent la même entité, disposer de l'un a le même effet que l'autre. –

2

Using définit un champ, à l'extérieur dont un objet est disposé, ainsi le ObjectDisposedException. Vous ne pouvez pas accéder au contenu du StreamReader en dehors de ce bloc.

0

Dispose() sur parent sera Dispose() tous les flux possédés. Malheureusement, les flux n'ont pas la méthode Detach(), vous devez donc créer une solution de contournement ici.

0

Je ne sais pas pourquoi, mais vous pouvez laisser votre StreamReader non disposé. De cette façon, votre flux sous-jacent ne sera pas éliminé, même lorsque StreamReader a été collecté.

+0

Bien sûr. Mais ce n'est pas trop intuitif de ne pas utiliser un 'StreamReader' pour moi, et cela viole probablement les règles de FxCop. En passant, le problème à l'origine de cette question est apparu lorsque j'ai refaçonné et ancien code où les utilisations étaient complètement manquantes. –

1

Je suis d'accord avec votre question. Le plus gros problème avec cet effet secondaire intentionnel est lorsque les développeurs ne le savent pas et suivent aveuglément la "meilleure pratique" d'entourer un StreamReader avec un using. Mais il peut causer certains vraiment difficile de traquer les bugs quand il est sur la propriété d'un objet à long terme, le meilleur (pire?) Exemple, je l'ai vu est

using (var sr = new StreamReader(HttpContext.Current.Request.InputStream)) 
{ 
    body = sr.ReadToEnd(); 
} 

Le développeur avait aucune idée de l'InputStream est maintenant arrosé pour tout futur endroit qui s'attend à être là.

De toute évidence, une fois que vous connaissez les internes que vous connaissez pour éviter le using et il suffit de lire et de réinitialiser la position. Mais je pensais qu'un principe de base de la conception de l'API était d'éviter les effets secondaires, en particulier de ne pas détruire les données sur lesquelles vous agissez. Rien d'inhérent à une classe censée être un "lecteur" ne devrait effacer les données lues une fois l'utilisation faite. La mise au rebut du lecteur devrait libérer toutes les références au Stream, ne pas effacer le flux lui-même. La seule chose que je peux penser est que le choix a dû être fait puisque le lecteur modifie l'autre état interne du Stream, comme la position du pointeur de recherche, qu'ils ont supposé si vous en enveloppez un autour, vous allez intentionnellement à être fait avec tout. D'un autre côté, comme dans votre exemple, si vous créez un flux, le flux lui-même sera dans un using, mais si vous lisez un flux qui a été créé en dehors de votre méthode immédiate, il est présomptueux du code de effacer les données.

Ce que je fais et dis à nos développeurs à faire sur les instances de flux que le code de lecture ne crée pas explicitement est ...

// save position before reading 
long position = theStream.Position; 
theStream.Seek(0, SeekOrigin.Begin); 
// DO NOT put this StreamReader in a using, StreamReader.Dispose() clears the stream 
StreamReader sr = new StreamReader(theStream); 
string content = sr.ReadToEnd(); 
theStream.Seek(position, SeekOrigin.Begin); 

(désolé, j'ajouté cela comme une réponse, ne correspondraient pas à un commentaire, j'aimerais plus de discussion sur cette décision de conception du cadre)

+2

Une meilleure conception pour un StreamReader aurait été d'avoir un argument constructeur qui spécifie s'il doit prendre possession du flux. Il existe de nombreuses situations où le code peut ouvrir un flux, en créer un lecteur, remettre le lecteur à quelque chose qui lira les données à un moment ultérieur, puis ne plus utiliser le flux. Dans cette situation, le flux doit être éliminé lorsque le lecteur en a terminé avec lui, mais le créateur du flux peut ne pas savoir quand cela sera le cas. – supercat