C# 溫故而知新:Stream篇(—)


  目錄:

什么是Stream?

什么是字節序列

Stream的構造函數

Stream的重要屬性及方法

Stream的示例

Stream異步讀寫

Stream 和其子類的類圖

本章總結

 

 

 

什么是Stream?

MSDN 中的解釋太簡潔了: 提供字節序列的一般視圖

(我可不想這么理解,這必定讓我抓狂,我理解的流是向自然界的河流那樣清澈而又美麗,c#中的流也是一樣,許多技術或者說核心技術都需要流的幫忙)

那什么是字節序列呢?

其實簡單的來理解的話字節序列指的是:

字節對象都被存儲為連續的字節序列,字節按照一定的順序進行排序組成了字節序列

那什么關於流的解釋可以抽象為下列情況:

打個比方:一條河中有一條魚游過,這個魚就是一個字節,這個字節包括魚的眼睛,嘴巴,等組成8個二進制,顯然這條河就是我們的核心對象:流

馬上進入正題,讓我們來解釋下c#的 Stream 是如何使用的

讓我們直接溫故或學習下Stream類的結構,屬性和相關方法

首先是構造函數

Stream 類有一個protected 類型的構造函數, 但是它是個抽象類,無法直接如下使用

   Stream stream = new Stream();

所以我們自定義一個流繼承自Stream 看看哪些屬性必須重寫或自定義:

View Code

可以看出系統自動幫我們實現了Stream 的抽象屬性和屬性方法

   1:  CanRead: 只讀屬性,判斷該流是否能夠讀取:

   2:  CanSeek: 只讀屬性,判斷該流是否支持跟蹤查找

   3:  CanWrite: 只讀屬性,判斷當前流是否可寫

*4: void Flush():這點必須說得仔細些:

    當我們使用流寫文件時,數據流會先進入到緩沖區中,而不會立刻寫入文件,當執行這個方法后,緩沖區的數據流會立即注入基礎流

     MSDN中的描述:使用此方法將所有信息從基礎緩沖區移動到其目標或清除緩沖區,或者同時執行這兩種操作。根據對象的狀態,可能需要修

     改流內的當前位置(例如,在基礎流支持查找的情況下即如此)當使用 StreamWriter 或 BinaryWriter 類時,不要刷新 Stream 基對象。

     而應使用該類的 Flush 或 Close 方法,此方法確保首先將該數據刷新至基礎流,然后再將其寫入文件。

(紅色部分為關鍵請大家務必能夠理解,今后會在相應的章節中介紹)

  5: Length:表示流的長度

*6: Position屬性:(非常重要)

雖然從字面中可以看出這個Position屬性只是標示了流中的一個位置而已,可是我們在實際開發中會發現這個想法會非常的幼稚,

很多asp.net項目中文件或圖片上傳中很多朋友會經歷過這樣一個痛苦:Stream對象被緩存了,導致了Position屬性在流中無法

找到正確的位置,這點會讓人抓狂,其實解決這個問題很簡單,聰明的你肯定想到了,其實我們每次使用流前必須將Stream.Position

設置成0就行了,但是這還不能根本上解決問題,最好的方法就是用Using語句將流對象包裹起來,用完后關閉回收即可。

*7: abstract int Read(byte[] buffer, int offset, int count)

這個方法包含了3個關鍵的參數:緩沖字節數組,位移偏量和讀取字節個數,每次讀取一個字節后會返回一個緩沖區中的總字節數

第一個參數:這個數組相當於一個空盒子,Read()方法每次讀取流中的一個字節將其放進這個空盒子中。(全部讀完后便可使用buffer字節數組了)

第二個參數:表示位移偏量,告訴我們從流中哪個位置(偏移量)開始讀取。

最后一個參數:就是讀取多少字節數。

返回值便是總共讀取了多少字節數.

*8: abstract long Seek(long offset, SeekOrigin origin)

    大家還記得Position屬性么?其實Seek方法就是重新設定流中的一個位置,在說明offset參數作用之前大家先來了解下SeekOrigin這個枚舉:

如果 offset 為負,則要求新位置位於 origin 指定的位置之前,其間隔相差 offset 指定的字節數。如果 offset 為零 (0),則要求新位置位於由 origin 指定的位置處。

如果 offset 為正,則要求新位置位於 origin 指定的位置之后,其間隔相差 offset 指定的字節數.

Stream. Seek(-3,Origin.End);  表示在流末端往前數第3個位置

Stream. Seek(0,Origin.Begin); 表示在流的開頭位置

Stream. Seek(3,Orig`in.Current); 表示在流的當前位置往后數第三個位置

查找之后會返回一個流中的一個新位置。其實說道這大家就能理解Seek方法的精妙之處了吧

*9: abstract void Write(byte[] buffer,int offset,int count)

這個方法包含了3個關鍵的參數:緩沖字節數組,位移偏量和讀取字節個數

和read方法不同的是 write方法中的第一個參數buffer已經有了許多byte類型

的數據,我們只需通過設置 offset和count來將buffer中的數據寫入流中

*10: virtual void Close()

關閉流並釋放資源,在實際操作中,如果不用using的話,別忘了使用完流之后將其關閉

這個方法特別重要,使用完當前流千萬別忘記關閉!

 

為了讓大家能夠快速理解和消化上述屬性和方法我會寫個示例並且關鍵部分會詳細說明

復制代碼
    static void Main(string[] args)
{
byte[] buffer = null;

string testString = "Stream!Hello world";
char[] readCharArray = null;
byte[] readBuffer = null;
string readString = string.Empty;
//關於MemoryStream 我會在后續章節詳細闡述
using (MemoryStream stream = new MemoryStream())
{
Console.WriteLine("初始字符串為:{0}", testString);
//如果該流可寫
if (stream.CanWrite)
{
//首先我們嘗試將testString寫入流中
//關於Encoding我會在另一篇文章中詳細說明,暫且通過它實現string->byte[]的轉換
buffer = Encoding.Default.GetBytes(testString);
//我們從該數組的第一個位置開始寫,長度為3,寫完之后 stream中便有了數據
//對於新手來說很難理解的就是數據是什么時候寫入到流中,在冗長的項目代碼面前,我碰見過很
//多新手都會有這種經歷,我希望能夠用如此簡單的代碼讓新手或者老手們在溫故下基礎
stream.Write(buffer, 0,3);

Console.WriteLine("現在Stream.Postion在第{0}位置",stream.Position+1);

//從剛才結束的位置(當前位置)往后移3位,到第7位
long newPositionInStream =stream.CanSeek? stream.Seek(3, SeekOrigin.Current):0;

Console.WriteLine("重新定位后Stream.Postion在第{0}位置", newPositionInStream+1);
if (newPositionInStream < buffer.Length)
{
//將從新位置(第7位)一直寫到buffer的末尾,注意下stream已經寫入了3個數據“Str”
stream.Write(buffer, (int)newPositionInStream, buffer.Length - (int)newPositionInStream);
}


//寫完后將stream的Position屬性設置成0,開始讀流中的數據
stream.Position = 0;

// 設置一個空的盒子來接收流中的數據,長度根據stream的長度來決定
readBuffer = new byte[stream.Length];


//設置stream總的讀取數量 ,
//注意!這時候流已經把數據讀到了readBuffer中
int count = stream.CanRead?stream.Read(readBuffer, 0, readBuffer.Length):0;


//由於剛開始時我們使用加密Encoding的方式,所以我們必須解密將readBuffer轉化成Char數組,這樣才能重新拼接成string

//首先通過流讀出的readBuffer的數據求出從相應Char的數量
int charCount = Encoding.Default.GetCharCount(readBuffer, 0, count);
//通過該Char的數量 設定一個新的readCharArray數組
readCharArray = new char[charCount];
//Encoding 類的強悍之處就是不僅包含加密的方法,甚至將解密者都能創建出來(GetDecoder()),
//解密者便會將readCharArray填充(通過GetChars方法,把readBuffer 逐個轉化將byte轉化成char,並且按一致順序填充到readCharArray中)
Encoding.Default.GetDecoder().GetChars(readBuffer, 0, count, readCharArray, 0);
for (int i = 0; i < readCharArray.Length; i++)
{
readString += readCharArray[i];
}
Console.WriteLine("讀取的字符串為:{0}", readString);
}

stream.Close();
}

Console.ReadLine();

}
復制代碼


顯示結果:

大家需要特別注意的是stream.Positon這個很神奇的屬性,在復雜的程序中,往往流對象操作也會很復雜,

一定要切記將stream.Positon設置在你所需要的正確位置,還有就是 using語句的使用,它會自動銷毀stream對象,

當然Stream.Close()大家都懂的

 

接着讓我們來說下關於流中怎么實現異步操作

在Stream基類中還有幾個關鍵方法,它們能夠很好實現異步的讀寫,

復制代碼
異步讀取
public virtual IAsyncResult BeginRead(byte[] buffer,int offset,int count,AsyncCallback callback,Object state)
異步寫
public virtual IAsyncResult BeginWrite( byte[] buffer, int offset, int count, AsyncCallback callback, Object state )
結束異步讀取
public virtual int EndRead( IAsyncResult asyncResult ) 
結束異步寫
public virtual void EndWrite( IAsyncResult asyncResult )  
復制代碼

大家很容易的就能發現前兩個方法實現了IAsyncResult接口,后2個end方法也順應帶上了一個IAsyncResult參數,

其實並不復雜,(必須說明下 每次調用 Begin方法時都必須調用一次 相對應的end方法)

和一般同步read或write方法一致的是,他們可以當做同步方法使用,但是在復雜的情況下可能也難逃阻塞崩潰等等,但是一旦啟用了

異步之后,這些類似於阻塞問題會不復存在,可見微軟對於異步的支持正在加大。

 

 最后是有關c#中Stream類和其子類的類圖

  類圖呢?大家肯定會這么想把 ^^

   為什么這個在目錄中是灰色的?其實我個人覺得這個類圖不應該放在這篇博文中,原因是我們真正理解並熟練操作了Stream的所有子類?(大牛除外)

  (這也是我寫后續文章的動力之一,寫博能很好的提升知識點的吸收,不僅能幫助別人,也能提高自己的對於知識點的理解),所以我想把類圖放在這

   個系類的總結篇中

 

本章總結:

本章介紹了流的基本概念和c#中關於流的基類Stream所包含的一些重要的屬性和方法,關鍵是一些方法和屬性的細節和我們操作流對象時必須注意的事項,

文中很多知識點都是自身感悟學習而來,深夜寫文不容易,請大家多多關注下,下一章將會介紹操作流類的工具:StreamReader 和StreamWriter

敬請期待!


免責聲明!

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



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