在第七章我們粗略介紹了一下Vec的用法。實(shí)際上,作為Rust中一個(gè)非常重要的數(shù)據(jù)類型,熟練掌握Vec的用法能大大提升我們?cè)赗ust世界中的編碼能力。
和我們之前接觸到的Array不同,Vec
具有動(dòng)態(tài)的添加和刪除元素的能力,并且能夠以O(1)
的效率進(jìn)行隨機(jī)訪問。同時(shí),對(duì)其尾部進(jìn)行push或者pop操作的效率也是平攤O(1)
的。
同時(shí),有一個(gè)非常重要的特性(雖然我們編程的時(shí)候大部分都不會(huì)考量它)就是,Vec的所有內(nèi)容項(xiàng)都是生成在堆空間上的,也就是說,你可以輕易的將Vec move出一個(gè)棧而不用擔(dān)心內(nèi)存拷貝影響執(zhí)行效率——畢竟只是拷貝的棧上的指針。
另外的就是,Vec<T>
中的泛型T
必須是Sized
的,也就是說必須在編譯的時(shí)候就知道存一個(gè)內(nèi)容項(xiàng)需要多少內(nèi)存。對(duì)于那些在編譯時(shí)候未知大小的項(xiàng)(函數(shù)類型等),我們可以用Box
將其包裹,當(dāng)成一個(gè)指針。
我們可以用std::vec::Vec::new()
的方式來聲明一個(gè)Vec。
let mut v1: Vec<i32> = Vec::new();
這里需要注意的是,new
函數(shù)并沒有提供一個(gè)能顯式規(guī)定其泛型類型的參數(shù),也就是說,上面的代碼能根據(jù)v1
的類型自動(dòng)推導(dǎo)出Vec
的泛型;但是,你不能寫成如下的形式:
let mut v1 = Vec::new::<i32>();
// 與之對(duì)比的,collect函數(shù)就能指定:
// let mut v2 = (0i32..5).collect::<Vec<i32>>();
這是因?yàn)檫@兩個(gè)函數(shù)的聲明形式以及實(shí)現(xiàn)形式,在此,我們不做深究。
相比調(diào)用new函數(shù),Rust提供了一種更加直觀便捷的方式聲明一個(gè)動(dòng)態(tài)數(shù)組: vec!
宏。
let v: Vec<i32> = vec![];
// 以下語句相當(dāng)于:
// let mut temp = Vec::new();
// temp.push(1);
// temp.push(2);
// temp.push(3);
// let v = temp;
let v = vec![1, 2, 3];
let v = vec![0; 10]; //注意分號(hào),這句話聲明了一個(gè) 內(nèi)容為10個(gè)0的動(dòng)態(tài)數(shù)組
因?yàn)閂ec實(shí)現(xiàn)了FromIterator
這個(gè)trait,因此,借助collect,我們能將任意一個(gè)迭代器轉(zhuǎn)換為Vec。
let v: Vec<_> = (1..5).collect();
就像數(shù)組一樣,因?yàn)閂ec借助Index
和IndexMut
提供了隨機(jī)訪問的能力,我們通過[index]
來對(duì)其進(jìn)行訪問,當(dāng)然,既然存在隨機(jī)訪問就會(huì)出現(xiàn)越界的問題。而在Rust中,一旦越界的后果是極其嚴(yán)重的,可以導(dǎo)致Rust當(dāng)前線程panic。因此,除非你確定自己在干什么或者在for
循環(huán)中,不然我們不推薦通過下標(biāo)訪問。
以下是例子:
let a = vec![1, 2, 3];
assert_eq!(a[1usize], 2);
那么,Rust中有沒有安全的下標(biāo)訪問機(jī)制呢?答案是當(dāng)然有:—— .get(n: usize)
(.get_mut(n: usize)
) 函數(shù)。
對(duì)于一個(gè)數(shù)組,這個(gè)函數(shù)返回一個(gè)Option<&T>
(Option<&mut T>
),當(dāng)Option==None的時(shí)候,即下標(biāo)越界,其他情況下,我們能安全的獲得一個(gè)Vec里面元素的引用。
let v =vec![1, 2, 3];
assert_eq!(v.get(1), Some(&2));
assert_eq!(v.get(3), None);
對(duì)于一個(gè)可變數(shù)組,Rust提供了一種簡(jiǎn)單的遍歷形式—— for 循環(huán)。 我們可以獲得一個(gè)數(shù)組的引用、可變引用、所有權(quán)。
let v = vec![1, 2, 3];
for i in &v { .. } // 獲得引用
for i in &mut v { .. } // 獲得可變引用
for i in v { .. } // 獲得所有權(quán),注意此時(shí)Vec的屬主將會(huì)被轉(zhuǎn)移??!
但是,這么寫很容易出現(xiàn)多層for
循環(huán)嵌套,因此,Vec
提供了一個(gè)into_iter()
方法,能顯式地將自己轉(zhuǎn)換成一個(gè)迭代器。然而迭代器怎么用呢?我們下一章將會(huì)詳細(xì)說明。
前面說到,Vec
有兩個(gè)O(1)
的方法,分別是pop
和push
,它們分別代表著將數(shù)據(jù)從尾部彈出或者裝入。理論上來說,因?yàn)?code>Vec是支持隨機(jī)訪問的,因此push
效率應(yīng)該是一致的。但是實(shí)際上,因?yàn)閂ec的內(nèi)部存在著內(nèi)存拷貝和銷毀,因此,如果你想要將一個(gè)數(shù)組,從零個(gè)元素開始,一個(gè)一個(gè)的填充直到最后生成一個(gè)非常巨大的數(shù)組的話,預(yù)先為其分配內(nèi)存是一個(gè)非常好的辦法。
這其中,有個(gè)關(guān)鍵的方法是reserve。
如下代碼(注意:由于SystemTime API在1.8以后才穩(wěn)定, 請(qǐng)使用1.8.0 stable 以及以上版本的rustc編譯):
use std::time;
fn push_1m(v: &mut Vec<usize>, total: usize) {
let e = time::SystemTime::now();
for i in 1..total {
v.push(i);
}
let ed = time::SystemTime::now();
println!("time spend: {:?}", ed.duration_since(e).unwrap());
}
fn main() {
let mut v: Vec<usize> = vec![];
push_1m(&mut v, 5_000_000);
let mut v: Vec<usize> = vec![];
v.reserve(5_000_000);
push_1m(&mut v, 5_000_000);
}
在筆者自己的筆記本上,編譯好了debug的版本,上面的代碼跑出了:
? debug git:(master) ? time ./demo
time spend: Duration { secs: 0, nanos: 368875346 }
time spend: Duration { secs: 0, nanos: 259878787 }
./demo 0.62s user 0.01s system 99% cpu 0.632 total
好像并沒有太大差異?然而切換到release版本的時(shí)候:
? release git:(master) ? time ./demo
time spend: Duration { secs: 0, nanos: 53389934 }
time spend: Duration { secs: 0, nanos: 24979520 }
./demo 0.06s user 0.02s system 97% cpu 0.082 total
注意消耗的時(shí)間的位數(shù)。可見,在去除掉debug版本的調(diào)試信息之后,是否預(yù)分配內(nèi)存消耗時(shí)間降低了一倍!
這樣的成績(jī),可見,預(yù)先分配內(nèi)存確實(shí)有助于提升效率。
有人可能會(huì)問了,你這樣糾結(jié)這點(diǎn)時(shí)間,最后不也是節(jié)省在納秒級(jí)別的么,有意義么?當(dāng)然有意義。
第一,納秒也是時(shí)間,這還是因?yàn)檫@個(gè)測(cè)試的Vec
只是最簡(jiǎn)單的內(nèi)存結(jié)構(gòu)。一旦涉及到大對(duì)象的拷貝,所花費(fèi)的時(shí)間可就不一定這么少了。
第二,頻繁的申請(qǐng)和刪除堆空間,其內(nèi)存一旦達(dá)到瓶頸的時(shí)候你的程序?qū)?huì)異常危險(xiǎn)。
更多Vec
的操作,請(qǐng)參照標(biāo)準(zhǔn)庫的api。