StreamWriter沒法訪問已經關閉的文件

文件讀寫和流程序員

一 流的概念
在.NET中Stream 是全部流的抽象基類。流是字節序列的抽象概念,或者說是計算機在處理文件或數據時產生的二進制序列。例如文件、輸入/輸出設備、內部進程通訊管道或者 TCP/IP 套接字。Stream 類及其派生類提供這些不一樣類型的輸入和輸出的通常視圖,使程序員沒必要了解操做系統和基礎設備的具體細節。簡單的說流提供了不一樣介質之間的數據交互功能。數組

在.NET中經常使用的流有BufferedStream 、FileStream、MemoryStream和NetworkStream,他們都是位於System.IO和System.NET命名空間下。流涉及三個基本操做: 讀取,寫入和查找。根據基礎數據源或儲存庫,流可能只支持這些功能中的一部分。有些流實現執行基礎數據的本地緩衝以提升性能。對於這樣的流,Flush 方法可用於清除全部內部緩衝區並確保將全部數據寫入基礎數據源或儲存庫。app

二 文件讀寫
對於文件的讀寫,實際是把硬盤中的數據讀入內存和把內存的數據寫入硬盤,他們數據之間的交換就是經過流來完成的。在.NET中這個功能是由FileStream類完成的。他提供的Write和Read方法能夠對文件進行讀寫操做。異步

1:FileStream讀寫文件
使用 FileStream 類對文件系統上的文件進行讀取、寫入、打開和關閉操做,並對其餘與文件相關的操做系統句柄進行操做,如管道、標準輸入和標準輸出。讀寫操做能夠指定爲同步或異步操做。FileStream 對輸入輸出進行緩衝,從而提升性能。
static void Main(string[] args)   
{   
    try  
    {   
        FileStream fs = new FileStream(@"c:\text.txt", FileMode.Create);   
        string message = "This is example for filestream";   
        byte[] writeMesaage = Encoding.UTF8.GetBytes(message);   
        fs.Write(writeMesaage, 0, writeMesaage.Length);   
    }   
    catch (Exception ex)   
    {   
        Console.WriteLine(ex.Message);   
    }   
    finally  
    {   
        Console.ReadKey();   
    }   
}  
static void Main(string[] args)
{
    try
    {
        FileStream fs = new FileStream(@"c:\text.txt", FileMode.Create);
        string message = "This is example for filestream";
        byte[] writeMesaage = Encoding.UTF8.GetBytes(message);
        fs.Write(writeMesaage, 0, writeMesaage.Length);
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
    finally
    {
        Console.ReadKey();
    }
}ide

上面是一個簡單的例子,把一條字符串寫入到文件中。首先創建一個FileStream對象,指定文件和讀寫方式(具體讀寫方式和權限能夠參加MSDN)。接下來把要寫入的字符串以必定的編碼格式存入一個字節數組中,而後調用Writer方法寫入文件。運行程序,當程序執行到Console.ReadKey方法時去查看文件發現文件中內容是空的。也就是說調用Writer方法後內容並無被寫入到文件中。函數

這裏就要談到流中的緩衝區的問題了。緩衝區是爲了提升I/O效率而設置的,咱們知道讀寫的I/O操做是很費時的,若是每個字節都立刻寫入到文件中整個過程就會很慢,因此設置緩衝區,寫把要寫入的內容寫入到緩衝區中,而後在一次性寫入到文件中,來提升寫入的效率和速度。而Write方法實際上只是把數據寫入到流的緩衝區中,而不是真正的寫入到文件中。因此調用Writer方法並不能完成文件的寫入。因而FileStream對象提供了一個把緩衝區寫入文件的方法,那就是Flush方法。性能

Flush:清除該流的全部緩衝區會使得全部緩衝的數據都將寫入到文件系統。這是MSDN給出的定義,能夠看到,只有調用了Flush方法後數據纔會被真正的寫入到文件中。因此這裏就又另一個問題,那就是可能存在寫入失敗。好比上面在Writer方法結束後發生了異常,那麼數據就沒法寫入到文件中了。因此咱們在調用Writer方法後能夠顯式的調用Flush方法來把數據寫入到文件中。可是上面的方法結束後又會發現數據被寫入了。其實這是由於在程序結束時,銷燬FileStream對象時,系統自動調用了Flush方法來保證內容被寫入到文件中。而在FileStream對象中,不少地方都調用了這個方法,好比Close方法和Dispose方法。因此在程序中,調用這2個方法銷燬對象時也會把數據從緩衝區寫入文件。因此使用FileStream對象Writer方法後只要不拋出異常,緩衝區數據總會被寫入文件(固然也可能由於磁盤已滿而在寫入是拋出異常)。可是咱們最好仍是顯示的調用Close方法或使用using塊關閉對象,使數據寫入。或是調用Flush方法。Flush方法內部調用API的internal static extern unsafe int WriteFile方法實現文件寫入。this

對於讀取文件內容也是相似的,要先把數據讀取到字節數組中。並且還提供了BeginRead和BeginWrite方法進行異步讀寫操做。編碼

 2 StreamWriter寫文件
上面的FileStream操做文件讀寫,每次都須要使用字節數組,由於FileStream操做對象是字節。而.NET提供了StreamWriter和StreamReader對象來對流進行讀寫操做。操作系統

他的構造函數能夠接受一個Stream對象。從而對流進行操做。他們的內部有個一Stream對象來維護傳入的各類流對象。而且也提供了Write和Read方法。實際上這2個類是對流讀寫的的一個包裝,方便咱們使用。當咱們傳一個流對象時,調用讀寫方法是,實際調用該對象本身重寫的方法。而當咱們在構造函數中傳入的是文件路徑時,他就成爲了對文件讀寫的操做。由於他在內部構建了一個FileStream對象,並交給內部的Stream對象維護。
public StreamWriter(string path) : this(path, false, UTF8NoBOM, 0x400)   
{   
}       
public StreamWriter(string path, bool append, Encoding encoding, int bufferSize) : base(null)  
{   
    if ((path == null) || (encoding == null))   
    {   
        throw new ArgumentNullException((path == null) ? "path" : "encoding");   
    }   
    if (bufferSize <= 0)   
    {   
        throw new ArgumentOutOfRangeException("bufferSize", Environment.GetResourceString("ArgumentOutOfRange_NeedPosNum"));  
    }   
    Stream stream = CreateFile(path, append);   
    this.Init(stream, encoding, bufferSize);   
}   
  
  
private static Stream CreateFile(string path, bool append)   
{   
    return new FileStream(path, append ? FileMode.Append : FileMode.Create, FileAccess.Write, FileShare.Read, 0x1000, FileOptions.SequentialScan);  
}  
public StreamWriter(string path) : this(path, false, UTF8NoBOM, 0x400)
{
}

public StreamWriter(string path, bool append, Encoding encoding, int bufferSize) : base(null)

{
    if ((path == null) || (encoding == null))
    {
        throw new ArgumentNullException((path == null) ? "path" : "encoding");
    }
    if (bufferSize <= 0)
    {
        throw new ArgumentOutOfRangeException("bufferSize", Environment.GetResourceString("ArgumentOutOfRange_NeedPosNum"));
    }
    Stream stream = CreateFile(path, append);
    this.Init(stream, encoding, bufferSize);
}

private static Stream CreateFile(string path, bool append)

{
    return new FileStream(path, append ? FileMode.Append : FileMode.Create, FileAccess.Write, FileShare.Read, 0x1000, FileOptions.SequentialScan);
}

經過上面的代碼,能夠看到咱們使用 public StreamWriter(string path)構造方法和咱們本身新建一個FileStream對象傳遞給StreamWriter(Stream)構造方法是同樣的。不一樣的是後者還可對其餘繼承與Stream的流進行操做。並且能夠指定文件讀取的方式和訪問權限以及緩衝區大小。

static void Main(string[] args)   
{   
    try  
    {   
        StreamWriter sw = new StreamWriter(@"c:\text.txt");   
        sw.Write("This is StreamWriter");   
    }   
    catch (Exception ex)   
    {   
        Console.WriteLine(ex.Message);   
    }   
    finally  
    {   
        Console.ReadKey();   
    }   
}  
static void Main(string[] args)
{
    try
    {
        StreamWriter sw = new StreamWriter(@"c:\text.txt");
        sw.Write("This is StreamWriter");
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
    finally
    {
        Console.ReadKey();
    }
}

上面的代碼是使用StreamWriter對文件進行寫操做,當執行到ReadKey時,咱們發現文件沒有被寫入,這個和FileStream是同樣的,可是當程序執行完後咱們發現,數據仍是沒有被寫入。若是咱們寫入的數據量比較大時,數據也被寫入到文件中,可是會發現寫入的數據可能並不完整。由於只有當StreamWriter內部的緩衝區充滿或調用Flush時,纔會把數據寫入Stream對象中。StreamWriter 未將最後 1 至 4 KB 數據寫到文件。後面會具體解釋。

MSDN中對此的解釋是:

StreamWriter 在內部緩衝數據,這須要調用 Close 或 Flush 方法將緩衝數據寫到基礎數據存儲區。若是沒有適當地調用 Close 或 Flush,StreamWriter 實例中緩衝的數據可能不會按預期寫出。

在StreamWriter中也有Flush方法,清理當前編寫器的全部緩衝區,並使全部緩衝數據寫入基礎流。對於StreamWriter來講,也有本身的緩衝區,而不一樣的是StreamWriter緩衝區是char[]而不是byte[]。而StreamWriter的write方法只是把數據寫入到本身的緩衝區中,因此咱們必須條用Flush方法來寫入到文件中,而Flush方法中則是先調用了FileStream的write方法把StreamWriter緩衝區的數據寫入到FileStream的緩衝區中,最後在調用FileStream的Flush方法寫入文件。

//StreamWriter.write把數據寫入StreamWriter緩衝區中   

public override void Write(string value)   
{   
    if (value != null)   
    {   
        int length = value.Length;   
        int sourceIndex = 0;   
        while (length > 0)   
        {   
            if (this.charPos == this.charLen)   
            {   
                this.Flush(false, false);   
            }   
            int count = this.charLen - this.charPos;   
            if (count > length)   
            {   
                count = length;   
            }   
            value.CopyTo(sourceIndex, this.charBuffer, this.charPos, count);   
            this.charPos += count;   
            sourceIndex += count;   
            length -= count;   
        }   
        if (this.autoFlush)   
        {   
            this.Flush(true, false);   
        }   
    }   
}   
  
//StreamWriter.Flush把StreamWriter緩衝區內容寫入Stream的緩衝區   
private void Flush(bool flushStream, bool flushEncoder)   
{   
    if (this.stream == null)   
    {   
        __Error.WriterClosed();   
    }   
    if (((this.charPos != 0) || flushStream) || flushEncoder)   
    {   
        if (!this.haveWrittenPreamble)   
        {   
            this.haveWrittenPreamble = true;   
            byte[] preamble = this.encoding.GetPreamble();   
            if (preamble.Length > 0)   
            {   
                this.stream.Write(preamble, 0, preamble.Length);   
            }   
        }   
        int count = this.encoder.GetBytes(this.charBuffer, 0, this.charPos, this.byteBuffer, 0, flushEncoder);  
        this.charPos = 0;   
        if (count > 0)   
        {   
            this.stream.Write(this.byteBuffer, 0, count);   
        }   
        if (flushStream)   
        {   
            this.stream.Flush();   
        }   
    }   
}  
//StreamWriter.write把數據寫入StreamWriter緩衝區中
public override void Write(string value)
{
    if (value != null)
    {
        int length = value.Length;
        int sourceIndex = 0;
        while (length > 0)
        {
            if (this.charPos == this.charLen)
            {
                this.Flush(false, false);
            }
            int count = this.charLen - this.charPos;
            if (count > length)
            {
                count = length;
            }
            value.CopyTo(sourceIndex, this.charBuffer, this.charPos, count);
            this.charPos += count;
            sourceIndex += count;
            length -= count;
        }
        if (this.autoFlush)
        {
            this.Flush(true, false);
        }
    }
}

//StreamWriter.Flush把StreamWriter緩衝區內容寫入Stream的緩衝區
private void Flush(bool flushStream, bool flushEncoder)
{
    if (this.stream == null)
    {
        __Error.WriterClosed();
    }
    if (((this.charPos != 0) || flushStream) || flushEncoder)
    {
        if (!this.haveWrittenPreamble)
        {
            this.haveWrittenPreamble = true;
            byte[] preamble = this.encoding.GetPreamble();
            if (preamble.Length > 0)
            {
                this.stream.Write(preamble, 0, preamble.Length);
            }
        }
        int count = this.encoder.GetBytes(this.charBuffer, 0, this.charPos, this.byteBuffer, 0, flushEncoder);
        this.charPos = 0;
        if (count > 0)
        {
            this.stream.Write(this.byteBuffer, 0, count);
        }
        if (flushStream)
        {
            this.stream.Flush();
        }
    }
}

經過上面的代碼能夠明白,真正完成寫入文件的也是Flush方法,由於它的工做是調用了FileStream的write和flush方法。而在StreamWriter的Close和Dispose的方法中則是調用了StreamWriter的Flush方法寫入文件,而後用FileStream.Close方法關閉流。

因此在關閉具備 StreamWriter 的實例的應用程序或任何代碼塊以前,確保調用 StreamWriter 的 Close 或 Flush。達到此目的的最佳機制之一是用 C# using 塊建立該實例,這樣將確保調用編寫器的 Dispose 方法,從而正確關閉該實例。另外在StreamWriter中有一個AutoFlush屬性,若是設置爲True,則在調用writer方法後會自動調用Flush方法。

3 FileStream和StreamWriter的依賴關係
若是咱們使用public StreamWriter(string path)構造方法不會存在這個問題,由於FileStrem對象是內部控制的,若是咱們用StreamWriter(Stream)構造方法就可能存在一些問題。

static void Main(string[] args)   
{   
    FileStream fs = null;   
        StreamWriter sw = null;   
    try  
    {   
        fs = new FileStream(@"c:\text.txt", FileMode.Create);   
        sw = new StreamWriter(fs);   
        string message = "This is StreamWriter\r\n";   
        for (int i = 0; i < 10; i++)   
        {   
            message += message;   
        }   
        sw.Write(message);   
    }   
    catch (Exception ex)   
    {   
        Console.WriteLine(ex.Message);   
    }   
    finally  
    {   
        fs.Close();   
        sw.Close();   
        Console.ReadKey();   
    }   
}  
static void Main(string[] args)
{
    FileStream fs = null;
        StreamWriter sw = null;
    try
    {
        fs = new FileStream(@"c:\text.txt", FileMode.Create);
        sw = new StreamWriter(fs);
        string message = "This is StreamWriter\r\n";
        for (int i = 0; i < 10; i++)
        {
            message += message;
        }
        sw.Write(message);
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
    finally
    {
        fs.Close();
        sw.Close();
        Console.ReadKey();
    }
}

執行上面的代碼的時候會出現Dispose異常,沒法訪問已經關閉的文件。這是由於咱們先關閉了文件流,而後在關閉StreamWriter對象。而StreamWriter對象的Close方法實際是關閉當前的 StreamWriter 對象和基礎流。也就是說咱們只需調用這一個方法就能夠了。而若是在數據寫入前調用了FileStream的Close方法,那麼數據最終是沒法寫入的,還會引起異常。因此在寫入文件時,最好只調用StreamWriter對象的Close方法就好了。

上面說過了沒有調用Close方法致使部分數據沒有寫入,這是因垃圾回收形成的。當咱們調用完write方法後,沒有調用close,系統發現StreamWriter和FileStream對象不可達,會對他們進行終結操做,可是終結的順序是不肯定的。若是先關閉了FileStream會出現數據沒法寫入。微軟爲了不這種狀況,就不讓StreamWriter方法實現Finalize方法,這樣,在程序結束時,沒有執行StreamWriter的Finalize方法,也就沒法把緩衝區的數據寫入FileStream中。而FileStream內部實現了Finalize方法。這也就是爲何FileStream不關閉仍然能夠把數據寫入文件。因此在使用StreamWriter對象時不顯調用Close方法時,緩衝區的數據必定會丟失。

並且WriterStream的內部緩衝區填滿後會自動寫入到Stream流中。因此當咱們寫入的數據不多時,不夠填充滿數據緩衝區,並且不關閉對象,必然沒法寫入文件。而當咱們寫大量數據時,一部分數據在緩衝區滿的時候被寫入了Stream中,當咱們不關閉對象,直接結束程序時,Stream會執行Finalize方法,把數據寫入文件,而StreamWriter沒有此方法,並且默認的緩衝區大小爲4K。若是此時緩衝區中還有數據一定沒法被寫入,並且大小是1-4K。

3 BinaryWriter
 BinaryWriter對象也能夠用寫文件,以二進制形式將基元類型寫入流,並支持用特定的編碼寫入字符串。與StreamWriter不一樣的是,他不存在緩衝區丟失的問題。由於他每次調用Write方法之後說首先把數據寫入本身的char[]數組,而後轉換爲指定編碼的Byte[]數組,最後調用Stream的Write方法寫入到流的緩衝區。

 BinaryWriter對象也有Flush方法,可是隻是簡單的調用了Stream的Flush方法,而他的Close和Dispose方法則是調用了Stream的Close方法。和上面同樣 BinaryWriter對象也沒有實現Finalize方法,可是由於他沒有把數據放到本身的緩衝區,每次都是當即寫入到流中。因此即使不調用Flush方法或是顯式關閉對象,最後也會所有被寫入到文件中,由於數據所有在FileStream的緩衝區中,而程序結束時Finalize方法會調用Flush把數據寫入文件。

 

轉自與http://blog.csdn.net/emixo/article/details/8041985

相關文章
相關標籤/搜索