C#中檔案流,網路流,緩衝流等流的概念理解
阿新 • • 發佈:2019-02-04
對於檔案的讀寫,實際是把硬碟中的資料讀入記憶體和把記憶體的資料寫入硬碟,他們資料之間的交換就是通過流來完成的。在.NET中這個功能是由FileStream類完成的。他提供的Write和Read方法可以對檔案進行讀寫操作。
1:FileStream讀寫檔案
使用 FileStream 類對檔案系統上的檔案進行讀取、寫入、開啟和關閉操作,並對其他與檔案相關的作業系統控制代碼進行操作,如管道、標準輸入和標準輸出。讀寫操作可以指定為同步或非同步操作。FileStream 對輸入輸出進行緩衝,從而提高效能。
view plaincopy to clipboardprint?
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物件維護。
view plaincopy to clipboardprint?
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的流進行操作。而且可以指定檔案讀取的方式和訪問許可權以及緩衝區大小。
view plaincopy to clipboardprint?
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方法寫入檔案。
view plaincopy to clipboardprint?
//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)構造方法就可能存在一些問題。
view plaincopy to clipboardprint?
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把資料寫入檔案。