【未經書面同意,嚴禁轉載】 -- 2020-10-13 --
Rust是系統編程語言。什么意思呢?其主要領域是編寫貼近操作系統的軟件,文件操作、辦公工具、網絡系統,日常用的各種客戶端、瀏覽器、記事本、聊天工具等,還包括硬件驅動、板載程序,甚至寫操作系統。但和python、Java等注重應用型語言不同。系統編程語言最主要的要求就是執行效率高、運行快!其次是可以訪問硬件,直接操作內存和各種端口。當前系統編程語言當推C和C++為老大,相對來說,C在更底層的驅動、嵌入式,C++側重在應用程序層。
這也注定了Rust的語法規則會比較多。另外,Rust站在諸多語言巨人的肩膀上,糅合了多家功力,為了解決現有語言的問題,提出了一些新的概念,可能有些規則讓學C++、Java等傳統語言的人大跌眼鏡。所以,我贊同一代宗師張三豐的方法:
《倚天屠龍記》
張三豐:“還記得嗎?”
張無忌:“全都記得。”
張三豐:“現在呢?”
張無忌:“已經忘了一小半。”
張三豐: “現在呢?”
張無忌: “啊,已經忘了一大半。”
張三豐:“不壞不壞,忘得真快,那么現在呢?”
張無忌: “已經全都忘了,忘得干干凈凈。”
好了,放空自己,放下過往,開始Rust,你會進步Fast!
從易到難,從根基到大廈。語言首先關注的,就應該是數據類型了。
Rust的數據類型特點:安全、高效、簡潔。除了類型的使用規范,編譯器的功能之強大,是保證這些特點的功臣。編譯器的首要任務是檢查類型使用正確與否,還具有類型推斷和支持泛型的特點,這使得Rust在高度限制的前提下又很靈活。
數據類型分類
Rust的基本類型(Primitive Types)有整型interger、字節byte、字符char、浮點型float、布爾bool、數組array、元組tuple(僅限於元組內的元素也是值類型)。在這里,所謂的基本類型,有以下特點:
- 數據分布在棧上,在參數傳遞的過程中會復制一個值用於傳遞,本身不會受影響;
- 數據在編譯時即可知道占用多大空間,比如i32占據4字節;
- 因為上2條的原因,數據存取特別快,執行效率高,但是棧空間比較小,不能存儲特別大的值。
后面要說的指針pointer、字符段str、切片slice、引用reference、單元unit(代碼中寫作一對小括號())、空never(在代碼中寫做嘆號!),也屬於基本類型,但是說起來比前面幾類復雜,本篇中講一部分,后面章節的內容還會融合這些數據類型。
除基本類型外最常用的類型是字符串String、結構體struct、枚舉enum、向量Vector和字典HashMap(也叫哈希圖)。string、struct、enum、vector、HashMap的數據都是在堆內存上分配空間,然后在棧空間分配指向堆內存的指針信息。函數也可以算是一種類型,此外還有閉包、trait。這些類型各有實現方式,復雜度也高。
這些數據的用法,就構成了Rust的語法規則。
下表是Rust的基本類型、常用的std庫內的類型和自定義類型。
類型寫法 | 描述 | 值舉例 |
i8, i16, i32, i64, u8, u16, u32, u64 |
i:帶符號 |
42, -5i8, 0x400u16, 0o100i16, 20_922_789_888_000u64, b'*' (u8 byte literal) |
isize, usize | 帶符號/無符號 整型 |
137, -0b0101_0010isize, 0xffff_fc00usize |
f32, f64 | IEEE標准的浮點數, 單精度/雙精度 |
1.61803, 3.14f32, 6.0221e23f64 |
bool | 布爾型 |
true, false |
char | Unicode字符 |
'*', '\n', '字', '\x7f', '\u{CA0}' |
(char, u8, i32) | 元組tuple:可以存儲多種類型 |
('%', 0x7f, -1) |
() | 單元類型,實際上是空tuple |
() |
struct S { x: f32, y: f32 } |
命名元素結構體,數據成員有變量名的結構體 |
struct S { x: 120.0, y: 209.0 } |
struct T(i32, char) | 元組型結構體,數據成員無名稱,形如元組,有點像python里的namedtuple |
struct T(120, 'X') |
struct E | 單元型結構體,沒有數據成員 |
E |
enum Attend { OnTime, Late(u32) } |
枚舉類型,例如一個Attend類型的值,要么取值OnTime,要么取值Late(u32) 與其他語言不通,枚舉類型默認沒有比較是否相等的運算,更沒有比較大小 |
Attend::Late(5), Attend::OnTime |
Box<Attend> | Box指針類型,指向堆內存中的一個泛型值 |
Box::new(Late(15)) |
&i32, &mut i32 | 只讀引用和可變引用,物所有權,生命周期不能超過所指向的值。 |
&s.y, &mut v |
String | 字符串,UTF-8格式存儲,長度可變 |
"編程".to_string() |
&str | str的引用,指向UTF-8文本的指針,無所有權 |
"そば: soba", &s[0..12] |
[f64; 4], [u8; 256] | 數組,固定長度,內部數據類型必須一致 |
[1.0, 0.0, 0.0, 1.0], [b' '; 256] |
Vec<f64> | Vector向量,可變長度,內部數據類型必須一致 |
vec![0.367, 2.718, 7.389] |
&[u8..u8], &mut [u8..u8] |
切片引用,通過起始索引和長度指向數組或向量的一部分連續元素 |
&v[10..20], &mut a[..] |
&Any, &mut Read | traid對象:實現了某trait內方法的對象 |
value as &Any, &mut file as &mut Read |
fn(&str, usize) -> isize |
函數類型,可以理解為函數指針 |
i32::saturating_add |
閉包 | 閉包 |
|a, b| a*a + b*b |
上表中沒有byte類型,是因為Rust壓根就沒有byte類型,實際上等於u8,在一般計算中認為是u8,在文件或網絡中讀寫數據時經常稱為byte流。
整型
Rust的帶符號整型,使用最高一位(bit)表示為符號,0為正數,1為負數,其他位是數值,用補碼表示。比如0b0000 0100i8,是正數,值為4,而0b1000 0100i8是負數,用補碼換算出來是-252。在此解釋一下這個數值的寫法,0b00000100i8,分成三部分來看:第一部分0b表示這個值是用2進制書寫的,0o開頭是8進制,0x開頭是16進制;第二部分00000100是數值;第三部分i8是類型,Rust中用數值后面直接跟類型(中間不能有空格),來表明這個數值是什么類型。另外,為了方便閱讀,數值中間或數值和類型中間可以加下划線,例如123_456i32, 5_1234u64, 8_i8,下划線只是為了便於人類閱讀,編譯時會忽略掉。
各整數類型的取值范圍:
u8: 0 至 28 –1 (0 至 255)
u16: 0 至 216 −1 (0 至 65,535)
u32: 0 至 232 −1 (0 至 4,294,967,295)
u64: 0 至 264 −1 (0 至 18,446,744,073,709,551,615,約1.8千億億)
usize: 0 至 232 −1 或 264 −1
i8: −27 至 27 −1 (−128 至 127)
i16: −215 至 215 −1 (−32,768 至 32,767)
i32: −231 至 231 −1 (−2,147,483,648 至 2,147,483,647)
i64: −263 至 263 −1 (−9,223,372,036,854,775,808 至 9,223,372,036,854,775,807)
isize: −231 至 231 −1, or −263 至 263 −1
因為帶符號整型的最高位是符號位,而無符號整型沒有符號位,所以能表示的最大正整數更大。
上面說過Rust沒有byte類型,而是u8類型。我們認為的byte型應該叫byte字面量,指的是ASCII字符,在本文中,暫且仍然稱為byte類型,書寫方式是b'x',b表示是byte,內容用單引號引起來。ASCII碼值在0至127(128至255也有ASCII字符,但是沒有統一標准,暫且不論)。一旦提到ASCII字符,就會想到一些不可見字符,比如回車、換行、制表符,就需要用一種可見的形式來書寫,我們稱之為轉義。byte字面量是用單引號引起來的,所以單引號也需要轉義,轉義是在一個字母或符號前加一個反斜杠,所以反斜杠自身也需要轉義。
ASCII字符 |
byte字面量的書寫 |
相當於的數值 |
單引號 ' |
b'\'' |
39u8 |
反斜杠 \ |
b'\\' |
92u8 |
換行 |
b'\n' |
10u8 |
回車 |
b'\r' |
13u8 |
制表符Tab |
b'\t' |
9u8 |
對於沒有轉義或轉義難以閱讀的byte字符,建議用16進制的數字表示,形如b'0xHH',其中HH是兩位的16進制數字來表示其ASCII碼。下面是ASCII碼速查表。
題內話:
Rust中,char類型和byte類型完全不一樣,char類型和上述所有的整型都不相同,不要混淆!
usize和isize類似於C語言中的size_t,它們的精度與平台的位數相同,在32位體系結構中長32位,在64位體系結構中長64位。那有什么用?Rust要求數組的索引必須是usize類型,在一些數據結構中,數組和向量的元素數也是usize型。
在debug模式下,Rust會對整型的計算溢出做檢查:
let big_val = std::i32::MAX; //MAX 是std::i32中定義的常量,表示i32型的最大值,即231-1
let x = big_val + 1; // 發生異常 panic: arithmetic operation overflowed
但是在release中,不會出現異常,而是返回二進制計算的結果。具體地說:
std::i32::MAX的二進制: 0111 1111 1111 1111 1111 1111 1111 1111
加1操作后 : 1000 0000 0000 0000 0000 0000 0000 0000
x讀取這個二進制數,補碼翻譯的結果就是一個負數,正最大值加1操作后,編程了負最小值。如果正最大值加2,就等於負最小值加1,依次類推。這就像一個環形跑道,跑完一圈回到原點,然后繼續往前跑。這稱為wrapping arithmetic,回環算術。但絕不應該用這種溢出的方式進行回環運算,而是應該用整型的內置函數wrapping_add():
let x = big_val.wrapping_add(1); // 結果是i32類型的負最小值,完美回到起點~~
這種回環運算在需要模運算的時候很有用,例如hash算法、加密、清零等。
as是一個運算符,可以從一種整型轉換為另一種整型,例如:
assert_eq!( 10_i8 as u16, 10_u16); // 正數值由少位數轉入多位數 assert_eq!( 2525_u16 as i16, 2525_i16); // 正數值同位數轉換 assert_eq!( -1_i16 as i32, -1_i32); // 負數少位轉多位執行符號位擴展 assert_eq!(65535_u16 as i32, 65535_i32); // 正數少位轉多位執行0位擴展(也可以理解為符號位擴展)
//由多位數轉少位數,會截掉多位數的高位,相當於多位數除以2^N的取模,其中N是少位數的位數
assert_eq!( 1000_i16 as u8, 232_u8); //1000的二進制是0000 0011 1110 1000,截掉左側8位,留下右側8位,是232
assert_eq!(65535_u32 as i16, -1_i16); //65535的二進制,16個0和16個1,截掉高位的16個0,剩下的全是1,全1的有符號補碼是-1
//同位數的帶符號和無符號相互轉化,存儲的數字並不動,只是解釋的方法不一樣
//無符號數,就是這個值;而有符號數,需要用補碼來翻譯 assert_eq!(-1_i8 as u8, 255_u8); //有符號轉無符號
assert_eq!(255_u8 as i8, -1_i8); //無符號轉有符號
有以上例子可以看出,as運算符在整型之間轉換時,對存儲的數字並不改動,只是把數讀出來的時候進行截取、擴展、或決定是否采用補碼翻譯。
同樣在整型和浮點型之間轉換時,也是不會改動存儲的數字,而是用不同類型的方式去解釋翻譯。
浮點型
Rust的浮點型有單精度浮點型f32和雙精度浮點型f64,浮點數全部是有符號,沒有無符號浮點數這一說。
根據IEEE 754-2008規范,浮點類型包括正無窮大和負無窮大、不同的正負零值(即有正0和負0兩種0)以及非數字值(NaN)。
f32的存儲空間占4個字節,有6位有效數字,表示的范圍大概介於 –3.4 × 1038 和 +3.4 × 1038之間。
f64的存儲空間占8個字節,有15位有效數字,表示的范圍大概介於 –1.8 × 10308 和 +1.8 × 10308 之間。
浮點數的書寫方式有123.4567和89.012e-2兩種寫法,后者叫科學技術法,89.012叫基數,-2叫尾數,e作為分隔,其值等於89.012× 10-2。
5.和.5都是合法的寫法。
如果一個浮點數缺少類型后綴,Rust會從上下文中推斷它是f32還是f64,如果兩者都可能,則默認為f64。在現代計算機中,對浮點數的計算做了優化,使用f64比f32的運算效率低不了太多。
但不會把一個整數推斷為浮點數。例如35是一個整數,35f32才是浮點數。
f32和f64類型都定義了一些特殊值常量: INFINITY(無窮大)、 NEG_INFINITY (負無窮)、NAN (非數字)、MIN(最小值)、MAX (最大值)。std::f32::consts和std::f64::consts模塊定義了一些常量:E(自然對數)、PI(圓周率)、SQRT_2(2的平方根)等等。
題外話:
上面這段使用了兩種說法:
- f32類型定義了……
意思是這些常量是在f32類型中定義的,f32在Rust的核心中,使用方法是f32::INFINITY、 f32::NEG_INFINITY、f32::MAX……
- std::f32::consts模塊定義了……
意思是這些常量是在std::f32::consts模塊定義的,這個模塊不屬於Rust核心,而是屬於std庫,使用方法:std::f32::consts::E……。或者使用后面講到的use std::f32::consts路徑導入,然后使用consts::E。
浮點數常用的一些方法:
assert_eq!(5f32.sqrt() * 5f32.sqrt(), 5.); // 平方根,此外還有sin()、ln()等諸多數學計算方法 assert_eq!(-3.7f64.floor(), -4.0); //向下取整,還有ceil()方法是向上取整,round()方法是四舍五入) assert_eq!(1.2f32.max(2.2), 2,2); //比較返回最大值,min()方法是取最小值
assert_eq!(-3.7f64.trunc(), -3.0); //刪除小數部分,注意和floor、ceil的區別
assert!((-1. / std::f32::INFINITY).is_sign_negative()); //是否為負值,注意-0.0也算負值
題內話:
代碼中通常不需要帶類型后綴,但在使用浮點類型的方法時,如果不標明類型,就會報編譯錯誤,例如:
println!("{}", (2.0).sqrt());
//編譯錯誤,錯誤提示:// error[E0689]: can't call method `sqrt` on ambiguous numeric type `{float}`
這種情況就需要標明類型,或者是使用關聯函數的方式調用:
println!("{}", (2.0_f64).sqrt()); //標明類型 println!("{}", f64::sqrt(2.0)); //浮點類型關聯函數的使用方法
與C和C++不同,RUST幾乎不執行數字隱式轉換。如果函數需要f64參數,則傳遞i32值作為參數是錯誤的。事實上,Rust甚至不會隱式地將i16值轉換為i32值,即使每個i16值可以無損擴展為i32值。這種情況的傳參需要用關鍵字as顯式地寫下:i as f64、 x as i32。缺乏隱式轉換有時會使Rust表達式比類似的C或C++代碼更冗長,但是隱式整數轉換極有可能導致錯誤和安全漏洞。
布爾型
我認為布爾型是最簡單的數據類型,只有true和false兩個值,所以只說幾個注意事項即可:
- Rust的判斷語句if,循環語句while的判斷條件,以及邏輯運算符&&和| |的表達式必須是布爾值,不能像C語言那樣 if 1{……},而是要寫成if 1!=0{……};
- as運算符可以將bool值轉換為整數類型,false轉換為0,true轉換為1;
- 但是,as不會從數值類型轉換為bool。你必須寫出一個像這樣的顯式比較x != 0;
- 盡管布爾值只需要1個位來表示它,但值的存儲使用整個字節(我相信任何一種有布爾類型的語言不會用1位來表示布爾型的,計算機尋址的方式就是按字節尋址)
就這些吧!
字符(char)
再次強調,不要把Rust中的字符char類型和byte混淆,更不要把字符串string認為是字符(char)的數組或序列!
原因是:Rust固定的用4字節來存儲char類型,表示一個Unicode字符。
而文本流(byte文本)和string是utf-8編碼的序列,UTF-8編碼是變長的。何為變長?就是一個Unicode字符,可能占1個字節,也可能占2、3個字節,例如英文字母a,Unicode碼是0x61,占1個字節,而中文“我”,Unicode碼是0xE68891,占3個字節。所以:
//string的len()方法返回字符串占據的字節數
String::from("a").len() //等於1 String::from("我").len() //等於3 String::from("a我").len() //等於4
和byte一樣,有些字符需要用反斜杠轉義。
char類型的書寫是用單引號引起來,字符串是用雙引號引起來:
‘C’——char類型;“C”——字符串;b'C'——byte型(u8型)
三者的存儲方式也不同:
char類型在棧內存上開辟4字節空間,把字母C的Unicode碼 0x 00 00 00 43存入;
byte型在棧內存上開辟1字節空間,把字母C的ASCII碼 0x43 存入;
字符串型在堆內存上開辟N字節空間(N一般是字母C的字節數1),然后在棧內存上開辟12字節空間(此處以32位平台為例),4個字節存放堆內存放置數據的指針,4個字節存放字符串在內存中開辟的空間N,4個字節存放字符串當前使用的空間。關於后兩個4字節的區別在string類型中敘述。
char類型的值包含范圍為0x0000到0xD7FF或0xE000到0x10FFFF的Unicode碼位。對於其他數值,Rust會認為是無效的char類型,出現編譯異常。
char不能和任何其他類型之間隱式轉換。但可以使用as運算符將字符轉換為整數類型;對於小於32位的類型,字符值的高位將被截斷:
assert_eq!('*' as i32, 42);
assert_eq!('ಠ' as u16, 0xca0); assert_eq!('ಠ' as i8, -0x60); // U+0CA0 被截斷為8位帶符號整型
所有的整數類型中,只有u8能用as轉換為char。如果想用u32位轉換為char,可以用std庫里的std::char::from_32()函數,返回值是Option<char>類型,關於這個類型我們后面會重點講述。
char類型和std庫的std::char模塊中有很多有用的char方法/函數,例如:
assert_eq!('*'.is_alphabetic(), false); //檢查是否是字母
assert_eq!('β'.is_alphabetic(), true);
assert_eq!('8'.to_digit(10), Some(8)); //檢查是否數字 assert_eq!('ಠ'.len_utf8(), 3); //用utf-8格式表示的話,占據幾個字節 assert_eq!(std::char::from_digit(2, 10), Some('2')); //數字轉換為char,第二個參數是進制
但是char類型使用的場景不多,我們應該更多關注相關的string類型。
元組 tuple
元組是若干個其他類型的數據,用逗號隔開,再用一對小括號包裹起來。例如(“巴西”, 1985, 29)。
首先,元組不是一種類型,我們只能說元組是一種格式,比如(“巴西”, 1985, 29)的類型是(&str, i32, i32),('p', 99)的類型是(char, i32),這兩者是不同的類型,明白這一點很重要,不同類型意味着不能直接判斷大小或是否相等。所以元組是無數個類型的統稱。
元組內的各個元素,可以用“.”來訪問,類似訪問對象中的成員:
let t = ("巴西", 1985, 29); let x = t.0 //"巴西" let y = t.1 //1985
由於元組的類型和各元素的類型相關,所以以下代碼是不對的
let mut t = ("巴西", 1985, 29); t.1 = 'A' //錯誤!!,t的類型是(&str, i32, i32),所以t.1賦值必須賦i32類型
通常使用元組類型從函數返回多個值:
let text = "I see the eigenvalue in thine eye"; //str型字符串 let (head, tail) = text.split_at(21); //此方法將一個str字符串分割為兩個str字符串,這兩個字符串就可以用一個二元的元組來接收 assert_eq!(head, "I see the eigenvalue "); assert_eq!(tail, "in thine eye");
我們經常將相關聯的幾個值以元組形式表示,比如一個坐標點,可以用(i64, i64)表示,三維坐標點可以用(i64, i64, i64)表示。
元組的一個特殊類型是空元組(), 也叫零元組(zero-tuple)或單元類型(unit),因為它只有一個值,就是()。Rust使用空元祖表示沒有有意義的值,但是上下文仍然需要某種類型的類型。例如,沒有顯式返回值的函數的返回類型為(),這在某些返回Result<>類型函數的時候,會有用。
元組的各元素用逗號分隔,而且在最末尾的元素后面,也可以加上逗號,例如(1, 2, 3,)。當元組中只有一個元素的時候,(1)就會有歧義了,編譯器會認為這是個括號表達式,而不是元組,而(1,)則明明白白是個元組。在Rust中,末尾元素可以加逗號的規則不但適合元組中,函數參數、數組、結構和枚舉定義等等場合也可以。
指針(Pointer)
作為系統編程語言,指針肯定是不能缺席。但是Rust為了實現安全的目的,對指針做了多個包裝類型,其中有安全指針(不會造成內存泄漏,Rust自動回收),也有不安全指針(由程序員負責分配和釋放)。Rust為了維護自己的尊嚴,聲稱大多數程序使用安全指針可以滿足。並對不安全指針也做了一些限制。
下面看一下兩種安全指針:引用(reference)和Box,和不安全指針(也叫裸指針,Raw Pointer)。
引用
引用的概念被廣泛使用,在Rust中,可以理解為指向某塊內存的指針。目標可以使堆空間也可以是棧空間。
目標值是某種類型,那指向目標的引用,也是有類型的。指向字符串string類型的引用是&string類型,指向i32類型的引用是&i32類型,即在目標值的類型前加&。
&不但用在引用類型的寫法上,而且用做引用運算符:
let a: i32 = 90; let ref_a: &i32 = &a; //&a就是a的引用
let ref_a2: &i32 = &a; //可以聲明多個引用
let b = *ref_a; // *是解引用運算符,即獲取某個引用的原始值
&和*運算符的作用和C語言中很像,但是Rust中引用不會是null,必須有目標值。
和普通變量相似,默認引用是不可變的,加上mut才能是可變。
和C語言指針的另一個主要的區別是,Rust跟蹤目標值的所有權和生命周期,引用不負責目標值的內存分配和釋放,只是一種借用關系,目標值根據自己的生命周期產生和銷毀,引用必須在目標值的生命周期內產生和使用,因此在編譯時可以排除懸空指針、多次釋放和指針無效等錯誤。
Box
用代碼在堆內存中分配空間的最簡單方法就是用Box::new
let t = (12, "eggs"); let b = Box::new(t); // 在堆內存中分配空間,容納一個元組,然后返回一個指向該內存段的Box型指針b
Box是一種泛型,t的類型是(i32,&str),因此b的類型是Box<(i32,&str)>。Box::new()分配足夠的內存來包含堆上的元組。這段堆空間的聲明周期就由變量b來控制,當b超出作用域時,內存將立即釋放。
裸指針 Raw Pointers
Rust也有裸指針類型 *mut T和 *const t。裸指針就像C++中的指針一樣。使用裸指針是不安全的,因為Rust不會追蹤它指向的內存。例如,裸指針可能為null,也可能指向已釋放的內存或包含不同類型值的內存。這些都是C++中典型的內存泄漏問題。
但是,只能在不安全代碼段中解引用裸指針。一個不安全代碼段是Rust針對特殊場合使用而加入機制,其安全性不能保證。
題外話:
Rust的內存安全依賴於強大的類型系統和編譯檢測,不過它並不能適應所有的場景。 首先,所有的編程語言都需要跟外部的“不安全”接口打交道,調用外部庫等,在“安全”的Rust下是無法實現的; 其次,“安全”的Rust無法高效表示復雜的數據結構,特別是數據結構內部有各種指針互相引用的時候;再次, 事實上還存在着一些操作,這些操作是安全的,但不能通過編譯器的驗證。
因此在安全的Rust背后,還需要
unsafe
代碼。它可以做三件事:
- 解引用裸指針
*const T
和*mut T
- 讀寫可變的靜態變量
static mut
- 調用不安全函數
unsafe代碼用unsafe{……}包括起來。
unsafe{ …… //unsafe 代碼 }
序列類型(數組Array、向量Vector、切片Slice)
Rust有三種類型用於表示內存中的序列值:
- 類型 [T;n] 表示一個由n個值組成的數組,每個值都是T類型。數組的大小是在編譯時確定的常量,是類型的一部分;數組的元素數量是固定的,不能增減。
- 類型 Vec<T> 稱為T的vector,是動態分配的、可增長的T類型值序列。vector的元素位於堆中,可以隨意調整vector的大小,增刪元素。
- 類型 &[T] 和 &mut[T] 稱為類型T的只讀切片和T的可變切片,是對序列中元素的引用,這些元素是序列的一部分,序列可以是數組或向量。切片相當於包含了指向此切片第一個元素的指針,以及切片元素數的計數。可變切片&mut[T]修改和增刪元素,但一個序列同時只能聲明一個切片;只讀切片&[T]不允許修改元素,但可以同時在一個序列上聲明多個只讀切片。
三種類型的值都有一個len()方法,返回這個序列的元素數;都可以用下標的方式訪問,形如v[0]……。Rust會檢查i是否在有效范圍內;如果沒有會產生異常。i必須是一個usize類型的值,不能使用任何其他整數類型作為索引。另外,它們的長度也可以是零。
數組Array
數組的初始化可以有形式:
let lazy_caterer: [u32; 6] = [1, 2, 4, 7, 11, 16]; //元素枚舉法 let taxonomy = ["Animalia", "Arthropoda", "Insecta"]; assert_eq!(lazy_caterer[3], 7); assert_eq!(taxonomy.len(), 3);
let mut sieve = [true; 10000]; //通項法,一共1000個元素,值都是true for i in 2..100 { if sieve[i] { let mut j = i * i; while j < 10000 { sieve[j] = false; j += i; } } } assert!(sieve[211]);
assert!(!sieve[9876]);
和元組一樣,數組的長度是其類型的一部分,在編譯時固定。不同長度的數組,不屬於同一類型。如果n是一個變量,則[true;n]不能表示數組。如果需要在運行時長度可變的數組,需要用到vector。
在數組上迭代元素時,常用的方法——迭代,搜索、排序、填充、篩選等——都以切片的方式出現,而不是數組。但是Rust在使用這些方法時隱式地將對數組的引用轉換為切片,因此可以在數組上直接調用任何切片的方法:
let mut chaos = [3, 5, 4, 1, 2];
chaos.sort();
assert_eq!(chaos, [1, 2, 3, 4, 5]);
sort方法實際上是在切片上定義的,但是由於sort通過引用獲取其操作數,所以我們可以直接在chaos中使用它:隱式地生成&mut[i32]切片。實際上前面提到的len方法也是一種切片方法。
Vector
Vec<T>是一個可調整大小的T類型元素序列,分配在堆上。
創建Vector的方式有:
let mut v = vec![2, 3, 5, 7]; //用vec!宏聲明 let mut w = vec![0; 1024]; //用通項法聲明 let mut x = Vec::new(); //用Vec的new函數(在Rust中,struct的new()方法相當於其他語言中的類構造函數) x.push("step"); //vector可以添加元素 x.push("on"); x.push("no"); x.push("pets"); assert_eq!(v, vec!["step", "on", "no", "pets"]); let y: Vec<i32> = (0..5).collect(); //根據迭代器創建,因為collect()方法可構建多種類型序列的值,所以變量y必須聲明類型 assert_eq!(v, [0, 1, 2, 3, 4]);
像數組一樣,vector可以使用切片的方法:
let mut v = vec!["a man", "a plan", "a canal", "panama"]; v.reverse(); assert_eq!(v, vec!["panama", "a canal", "a plan", "a man"]); //元素順序翻轉
在這里,reverse方法實際上是在切片類型上定義的,但是vector被隱式地引用了,變為施加在&mut[&str]切片上的方法。
Vec是一種非常常用、非常重要的類型,它幾乎可以用於任何需要動態長度的地方,因此還有許多其他方法可以創建向量或擴展現有的向量。
Vec<T>由三個值組成:指向分配給堆內存緩沖區的指針;緩沖區有能力存儲的元素數量;以及它現在實際包含的數量。隨着長度增加,當緩沖區達到,向向量添加另一個元素需要分配一個更大的緩沖區,將當前內容復制到其中,更新向量的指針和容量以描述新的緩沖區,最后釋放舊的緩沖區。
由於這個原理,假設建一個空vector,然后不斷往里添加元素。如果用 Vec::new()或者vec![]創建,將會頻繁調整vector的大小,因此就會在內存中不斷遷移。這時候,最好的辦法是能夠預估總的vector有多大,一次性申請空間,再添加元素的時候就盡量不重新分配空間,或者少重分配。方法Vec::with_capacity(capacity: usize
)能夠創建一個有初始化容量的vector,參數capacity代表能存放多少元素。(當然當達到這個數字時,並不是存不進去,而是會找塊更大的內存)
let mut v = Vec::with_capacity(2);
assert_eq!(v.len(), 0); assert_eq!(v.capacity(), 2); //初始化就有2個空座位 v.push(1); v.push(2); assert_eq!(v.len(), 2); assert_eq!(v.capacity(), 2); v.push(3); //空座位不夠時,再添加元素會重新分配空間 assert_eq!(v.len(), 3); assert_eq!(v.capacity(), 4); //一般是按空間翻倍分配,這是通常情況的最優算法
//在測試以上代碼環境中,也可能不是這個結果,Vec和系統的堆分配器可能會對請求進行取
vector的功能還有:
let mut v = vec![10, 30, 50]; // 在索引2處插入35 v.insert(2, 35); assert_eq!(v, [10, 30, 35, 50]); // 移除索引1的元素 v.remove(1); assert_eq!(v, [10, 35, 50]); let mut v = vec!["carmen", "miranda"];
//pop方法移除最后的元素並返回一個Option值,有值時返回Option::Some(val),無值時返回Option::None assert_eq!(v.pop(), Some(50));
assert_eq!(v.pop(), Some(35)); assert_eq!(v.pop(), Some(10));
assert_eq!(v.pop(), None);
題外話:
Option是一個枚舉(Enum)類型,在Rust中非常常用,致力於嚴格的數據安全規則。有兩個元素Some(T)和None,定義是(其中T是指任意一種數據類型):
enum Option<T> {
Some(T),
None,
}Option枚舉可以包裝一個值,例如一個i32類型的變量a,在一些情況下,可能有個整數值,在一些情況下可能是空值,但又不能用0來表達。Rust沒有其他語言中的null或None來表示。這時候,就可以使a賦值為Option枚舉類型。在空值的情況下,a = Option::None;在有值時,a=Option::Some(1024),數值寫在Some后的括號內。
不用null而是用option類型,是為了嚴格的數據類型安全,可以避免很多bug。
Option<T> 枚舉非常有用,以至於Option作用域已經在語言中內置了,你不需要將其顯式引入作用域。它的成員也是如此,可以不需要 Option:: 前綴來直接使用 Some 和 None。今后我們將直接用Some(xxx)和None。
vector 的元素遍歷可以用for ... in語句:
for i in vec!["C", "C++", "Python", "Go"] { println!("語言:{}", i); }
/* 輸出:
語言:C
語言:C++
語言:Python
語言:Go
*/
總結一下:
- vector的數據分布在堆上,棧上保存其指針;
- 可以用new方法或vec!宏或一些其他方法創建;
- 如果能預估總長度,最好使用Vec::with_capacity()方法創建;
- 有增刪改查元素以及排序、翻轉、迭代等等方法;
- 各種命名(注意大小寫):Vec是向量的類型名,就像i32、char類似;vec!是創建向量的宏;vector只是向量的英文單詞,不是關鍵字。
數據類型的上篇到此為止吧,下篇說一下切片Slice、字符串String和文本字符串str。另外,函數在語句篇介紹,trait和閉包有專門的篇章。
說實話,Rust的入門內容挺多,很多特點和其他語言不同,這就是所謂的學習路線陡峭吧。就感覺處處都是知識點,沒這些知識點做鋪墊,連一個小小的demo都寫不了。
但這些都是基礎的點,雖然多,學起來還是很容易的。關鍵還是我在本系列文章的一開始說的:要有空杯心態,把其他語言的習性放一放,不強行混為一談。
對於喜歡學習的人,有這么一門新鮮的語言,也算是一種福氣。