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


文件讀寫和流

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

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

二 文件讀寫
對於文件的讀寫,實際是把硬盤中的數據讀入內存和把內存的數據寫入硬盤,他們數據之間的交換就是通過流來完成的。在.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();
    }
}

上面是一個簡單的例子,把一條字符串寫入到文件中。首先建立一個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方法實現文件寫入。

對於讀取文件內容也是類似的,要先把數據讀取到字節數組中。而且還提供了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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM