楔子 Rust 標準庫包含了一系列非常有用的被稱為集合的數(shù)據(jù)結構,與內置的數(shù)組與元組不同,這些集合將自己持有的數(shù)據(jù)存儲在了堆上,這意味著數(shù)據(jù)的大小不需要在編譯時確定,并且可以隨著程序的運行按需擴大或縮小數(shù)據(jù)占用的空間。 不同的集合類型有著不同的性能特性與開銷,我們需要學會如何為特定的場景選擇合適的集合類型。Rust 當中主要有 3 個被廣泛使用的集合:
本文我們先來介紹動態(tài)數(shù)組。 創(chuàng)建動態(tài)數(shù)組 動態(tài)數(shù)組允許你在單個數(shù)據(jù)結構中存儲多個相同類型的值,這些值會彼此相鄰地排布在內存中。動態(tài)數(shù)組非常適合在需要存儲一系列相同類型值的場景中使用,例如商品信息或銷售金額等等。 我們可以調用函數(shù) Vec::new 來創(chuàng)建一個空動態(tài)數(shù)組:
注意這段代碼顯式地指明了變量的類型,因為我們還沒有在這個動態(tài)數(shù)組中插入任何值,所以 Rust 無法自動推導出我們想要存儲的元素類型,這一點非常重要。 另外動態(tài)數(shù)組在實現(xiàn)時使用了泛型,我們將在后續(xù)學習如何為自定義類型添加泛型。但就目前而言只需要知道,標準庫中的 Vec<T> 可以存儲任何類型的元素,至于我們想存儲哪一種,就把 T 換成相應的類型即可。 因此上面的語句向 Rust 傳達了這樣的含義:變量 v 是 Vec 類型、也就是動態(tài)數(shù)組,而且數(shù)組里面的元素類型是 i32。如果想存儲 f64,那么就聲明為 Vec<f64> 即可,總之 T 可以代表任意類型,至于到底代表哪一種,就看我們要存儲哪一種類型的元素。 不過在實際的編碼過程中,只要你向動態(tài)數(shù)組內插入了數(shù)據(jù),Rust 便可以在絕大部分情形下推導出你希望存儲的元素類型,我們只需要在極少數(shù)的場景中對類型進行聲明。
另外,使用初始值去創(chuàng)建動態(tài)數(shù)組的場景也十分常見,為此 Rust 特意提供了一個用于簡化代碼的 vec! 宏。這個宏可以根據(jù)我們提供的值來創(chuàng)建一個新的動態(tài)數(shù)組:
由于 Rust 可以推斷出我們提供的是 i32 類型的初始值,并進一步推斷出 v 的類型是 Vec<i32>,所以在這條語句中不需要對類型進行聲明。當然我們也可以顯式地標注類型:
以上就是動態(tài)數(shù)組的創(chuàng)建,下面來看看如何往動態(tài)數(shù)組中添加元素。 往動態(tài)數(shù)組添加元素 如果想往動態(tài)數(shù)組中添加元素,我們可以使用 push 方法。
為了在創(chuàng)建動態(tài)數(shù)組后將元素添加至其中,我們可以使用 push 方法。正如之前討論過的,對于任何變量,只要我們想要改變它的值,就必須使用關鍵字 mut 來將其聲明為可變的。 至于修改動態(tài)數(shù)組的元素,直接像普通數(shù)組那樣使用索引去修改即可。 銷毀動態(tài)數(shù)組也會銷毀內部的元素 和其他的 struct 一樣,動態(tài)數(shù)組一旦離開作用域就會被立即銷毀。
動態(tài)數(shù)組中的所有內容都會隨著動態(tài)數(shù)組的銷毀而銷毀,其持有的整數(shù)將被自動清理干凈。這一行為看上去也許較為直觀,但當接觸到指向動態(tài)數(shù)組元素的引用時就變得有些復雜了,一會兒我們來處理這種情況。 讀取動態(tài)數(shù)組的元素 了解完如何去創(chuàng)建、更新及銷毀動態(tài)數(shù)組,接下來就是讀取了。有兩種方法可以引用存儲在動態(tài)數(shù)組中的值:
然后需要注意的是,我們在獲取元素的時候盡量獲取它的引用,而不是獲取值本身,舉個例子:
上述代碼是會發(fā)生編譯錯誤的,要解釋這個問題,我們需要先回顧一下以前的內容。我們說過像整數(shù)、浮點數(shù)這種數(shù)據(jù),它們完全存儲在棧上面,而棧上的數(shù)據(jù)在傳遞的時候一律會拷貝一份。所以在變量賦值之后,兩個變量持有的是不同的數(shù)據(jù),因此這兩個變量都可以使用,而這樣的數(shù)據(jù)我們稱之為是可 Copy 的。 而對于 String 這種數(shù)據(jù),都是指針在棧上,然后指針指向堆區(qū)的數(shù)據(jù)。而堆區(qū)的數(shù)據(jù)默認是不會拷貝的,因此變量傳遞之后,兩個變量會引用同一份堆區(qū)數(shù)據(jù)。在 Python 里面會通過引用計數(shù)來記錄堆數(shù)據(jù)有幾個變量引用它,而 Rust 則是直接轉移所有權(操作堆內存的權利),保證同一時刻只能有一個變量可以操作堆內存。 變量的所有權一點轉移,那么它就失去了操作堆內存的權利,于是這個變量就不能再用了。等它離開作用域后,把它在棧上的數(shù)據(jù)銷毀即可,至于堆數(shù)據(jù)和它就沒關系了。如果希望它在賦值給別的變量之后還能繼續(xù)用,那么就調用 clone 方法,把堆上的數(shù)據(jù)也拷貝一份(Rust 默認不會拷貝堆數(shù)據(jù),需要開發(fā)者顯式調用某個方法進行拷貝)。 因此對于 String 這樣的數(shù)據(jù),我們說它是可 Clone 的。 所以再來看一下 let first = v[0] 為什么會報錯,如果 v 里面的數(shù)據(jù)是可 Copy 的,那么不會有任何問題,因為大家持有的數(shù)據(jù)是各自獨立的。但現(xiàn)在 v 里面的數(shù)據(jù)是可 Clone 的,因此只會默認拷貝棧上數(shù)據(jù),堆上數(shù)據(jù)是不會拷貝的,此時只能轉移所有權。但問題是,如果一個元素的所有權被轉移了, 那么整個數(shù)組就都不能用了,舉個例子:
其實這是 Rust 的一個機制,單獨的變量在轉移所有權的時候是沒問題的,但要轉移數(shù)組某個元素的所有權就不行了。因為一旦某個元素的所有權轉移,那么該元素就不能再用了,而它又在數(shù)組里面,進而導致整個數(shù)組變得不可使用。 這樣當我們再使用數(shù)組獲取其它元素的時候就會報錯,而這就會讓人產(chǎn)生疑惑,為啥數(shù)組好端端的,突然就不能訪問了呢?因此為了避免這個隱藏的 bug,Rust 干脆不允許轉移數(shù)組內部元素的所有權。 因此執(zhí)行 let first = v[0] 這種代碼時,Rust 會認為數(shù)組 v 里面的元素都是可 Copy 的,也就是數(shù)據(jù)全部在棧上,不涉及堆,只有這樣這行代碼才是成立的。由于數(shù)據(jù)全部在棧上,那么拷貝完之后,你的是你的,我的是我的,彼此互不影響。而如果不是可 Copy 的,那么 Rust 就會報錯。 那么我們該怎么做呢?很簡單,獲取它的引用不就好了。
這段代碼是沒有問題的,因為我們沒有獲取數(shù)組元素的所有權。 當然也可以使用 get,因為它獲取的就是引用,并且返回的還是 Option 枚舉。當指定的索引超出范圍時會返回 None,而不會報出索引越界錯誤。
而如果是通過中括號來訪問,那么索引越界就會引發(fā)崩潰。 一旦程序獲得了數(shù)組中某個元素的引用,借用檢查器就會執(zhí)行所有權規(guī)則和借用規(guī)則,來保證這個引用及其它任何指向這個動態(tài)數(shù)組的引用始終有效。回憶一下所有權規(guī)則,我們不能在同一個作用域中同時擁有可變引用與不可變引用。 而在下面這個例子中,我們持有了一個指向動態(tài)數(shù)組中首個元素的不可變引用,但卻依然嘗試向這個動態(tài)數(shù)組的結尾處添加元素,該嘗試是不會成功的。
編譯這段代碼將會導致下面的錯誤: 你也許會好奇,為什么獲取第一個元素的引用需要關心動態(tài)數(shù)組結尾處的變化呢?此處的錯誤是由動態(tài)數(shù)組的工作原理導致的:動態(tài)數(shù)組中的元素是連續(xù)存儲的,但如果已經(jīng)沒有空間在尾部添加新元素了,那么就需要分配新的內存空間,并將舊的元素移動過去。所以在本例中,第一個元素的引用可能會因為插入行為而指向被釋放的內存,借用規(guī)則可以幫助我們規(guī)避這類問題。 最后還是要指出,get 方法獲取的是引用,不管數(shù)組的元素是不是可 Copy 的,它獲取的都是引用。
整個過程應該不難理解。 遍歷動態(tài)數(shù)組的元素 假如你想要依次訪問動態(tài)數(shù)組中的每一個元素,那么可以采用遍歷的方式,而不需要使用索引來一個一個地訪問它們。
遍歷方式和 Python 類似,但上面代碼存在一個問題,就是在遍歷結束之后 v 就不能再用了。因為動態(tài)數(shù)組是申請在堆上的,在遍歷的時候會拿到它的所有權,因此正確的做法應該是獲取它的引用,然后遍歷。
對于整數(shù)這樣的標量來說,打印它的引用和打印它本身,效果是一樣的。但如果要修改的話,就不一樣了,舉個例子。
為了使用 *= 運算符來修改可變引用指向的值,我們需要使用解引用運算符(*)來獲得 i 綁定的值。如果是打印、調用方法的話,直接使用引用即可,會自動操作指向的值;但對加減法來說,需要先解引用才可以,因為引用直接不能進行運算。 再來總結一下遍歷時的注意事項: 1)如果遍歷的是 v,也就是動態(tài)數(shù)組本身,那么遍歷結束之后 v 將無效。因為在遍歷的時候,它的所有權就被剝奪了。并且無論數(shù)組里面的元素是不是可 Copy 的,都無關緊要,如果是可 Copy 的,那么遍歷的時候就拷貝一份;不是可 Copy 的,那就獲取所有權。咦,之前不是說不能獲取所有權嗎?很簡單,因為之前是單獨獲取數(shù)組的某一個元素,為了不影響整個數(shù)組,所以不能讓數(shù)組元素將所有權交出去。但現(xiàn)在是遍歷,由于遍歷之后數(shù)組就無效了,所以此時允許獲取內部元素的所有權。 2)如果遍歷的是 &v,那么遍歷之后 v 還可以繼續(xù)使用,因為所有權并沒有交出去。并且遍歷的是數(shù)組的引用,那么拿到的也是數(shù)組每一個元素的引用,而且是不可變引用。不管數(shù)組里的元素是不是可 Copy 的,拿到的都是引用。這樣當遍歷結束時,v 還可以繼續(xù)使用。 3)如果遍歷的是 &mut v,那么和遍歷 &v 類似,只不過拿到的是元素的可變引用。當然在聲明 v 的時候也要使用 mut,因為要獲取可變引用,那么變量一定是可變的。不管是變量整體改變(重新賦值),還是修改內部數(shù)據(jù),都要聲明為 mut。 所以在遍歷的時候,細節(jié)還是蠻多的。初次接觸的時候,很多人都會因為對所有權和引用不是特別熟,而感到難以理解。 最后我們再來補充一點,舉個例子:
雖然遍歷的是 &v,拿到的是 &i32,但變量是 &i。如果變量是 i,那么顯然它是 &i32,這沒問題;但如果是 &i,那么 &i 對應 &i32,因此 i 就是 i32。 可能這里有一些繞,總之雖然遍歷得到的是引用,但我們可以通過 &i 的方式拿到值。并且通過這種方式遍歷,要求數(shù)組里的元素是可 Copy 的。因為這種方式的目的很明確,就是在遍歷的時候將元素拷貝一份,所以它要求元素必須是可 Copy 的,也就是數(shù)據(jù)都在棧上,否則報錯。 此時就報錯了,而且信息很明顯,告訴我們元素不是可 Copy 的。因為拷貝的時候只拷貝棧上的數(shù)據(jù),而遍歷之后的 i 是 String 類型,它的數(shù)據(jù)還涉及到堆,但 Rust 默認又不會拷貝堆上數(shù)據(jù),因此報錯。 如果不報錯,面對不可 Copy 的數(shù)據(jù)也允許這種遍歷方式的話,那么變量 i 只能將數(shù)組里元素的所有權挨個奪走。并且遍歷結束之后,數(shù)組不能再用了,因為內部的元素都失去所有權了。但我們遍歷的是引用啊,而之所以遍歷引用,就是為了能在遍歷結束之后繼續(xù)使用數(shù)組,所以就矛盾了。于是 Rust 要求元素是可 Copy 的,數(shù)據(jù)必須全部在棧上,這樣拷貝之后彼此互不影響。 以上就是遍歷動態(tài)數(shù)組的一些細節(jié),在初次接觸的時候,如果感到有些云里霧里的非常正常。因為 Rust 本身就不好學,我們唯一能做的就是多動手敲一敲。 動態(tài)數(shù)組結合枚舉 在最開始的時候,我們說動態(tài)數(shù)組只能存儲相同類型的值,這個限制可能會帶來不小的麻煩,實際工作中總是會碰到需要存儲一些不同類型值的情況。但幸運的是,當我們需要在動態(tài)數(shù)組中存儲不同類型的元素時,可以定義并使用枚舉來應對這種情況,因為枚舉中的所有成員都被視為同一種枚舉類型。 假設我們希望讀取表格中的單元值,這些單元值可能是整數(shù)、浮點數(shù)或字符串,那么就可以使用枚舉的不同成員來存放不同類型的值。所有的這些枚舉成員都會被視作統(tǒng)一的類型:也就是這個枚舉類型。然后,我們便可以創(chuàng)建一個持有該枚舉類型的動態(tài)數(shù)組來存放不同類型的值。
為了計算出元素在堆上使用的存儲空間,Rust 需要在編譯時確定動態(tài)數(shù)組的類型。使用枚舉的另一個好處在于它可以顯式地列舉出所有可以被放入動態(tài)數(shù)組的值類型,然后再搭配 match 表達式,Rust 就可以在編譯時確保所有可能的情形都得到妥當?shù)奶幚怼?/span> 如果你沒有辦法在編寫程序時窮盡所有可能出現(xiàn)在動態(tài)數(shù)組中的值類型,那么就無法使用枚舉。為了解決這一問題,我們需要用到在后續(xù)會介紹的 trait。 以上就是動態(tài)數(shù)組的內容,當然我們這里都只是介紹了結構本身,其支持的方法我們還沒有說,比如除了 push 添加元素,還有 pop 刪除末尾的元素等等。我們會抽個時間專門去介紹這些方法,當然這些你也可以從標準庫的 API 文檔中進行查看。 或者你也可以使用 IDE: 我們看到方法非常多,可以自己試一試。 然后下一篇文章我們來介紹函數(shù)與閉包,至于更深入的字符串內容以及 HashMap,以后再介紹。 |
|