Rust 勸退系列 05:復合數(shù)據(jù)類型
閱讀本文大概需要 8 分鐘。
大家好,我是站長 polarisxu。
這是 Rust 勸退系列的第 5 個教程,探討 Rust 中的復合數(shù)據(jù)類型(Compound types)。Rust 中有兩種原生的復合類型:元組(tuple)和數(shù)組(array),順帶介紹切片。
01 元組類型
Go 語言沒有元組類型,但多返回值有點類似元組(但還是有區(qū)別的哦)。Python 中有元組類型,因此如果你熟悉 Python,對元組應該很熟悉。
什么是元組類型?
元組是一個可以包含各種類型的值的組合。元組是一個將多個其他類型的值組合進一個復合類型的主要方式。元組長度固定:一旦聲明,其長度無法增大或縮小。元組的類型由各組成元素類型的序列定義。
元組通過小括號定義,里面的元素通過逗號分隔,例如:
(23.2, 27, 'a');
這個字面值元組的類型是:(f64, i32, char),即對應每個元素的默認類型。因此,我們可以通過 let 將這個元組綁定到變量上,Rust 會進行類型推斷:
let tup = (23.2, 27, 'a');
在 VSCode 中可以看到 tup 的類型就是:(f64, i32, char)。同樣地,我們也可以為 tup 使用類型注解:
let tup: (f32, i8, char) = (23.2, 27, 'a');
因為元組是多個類型的集合,對元組中的類型沒有限制。因此,可以嵌套。比如:
(2, (2.1, 'a'), false);
不過建議別嵌套太多,否則可讀性太差。
如何訪問元組元素呢?
上面說,Go 語言中函數(shù)多返回值類似元組,在接收多返回值時,通過多個變量接收,比如:
// Go 語言
f, err := os.Open("abc.txt")
在 Rust 中,可以解構元組(這也叫模式匹配解構):
let tup = (23.2, 27, 'a');
let (x, y, z) = tup; // 注意:需要小括號
和 Go 語言一樣,如果某個元素我們不關心,可以放入垃圾桶(_):
let tup = (23.2, 27, 'a');
let (x, _, z) = tup; // 注意:需要小括號
Rust 中變量定義未使用,不會像 Go 一樣報錯,但會警告!
除了模式匹配解構,還可以使用類似訪問數(shù)組元素的方式訪問元組元素,只不過不是用[],而是用 . 加索引的方式(索引也是從 0 開始):
let tup = (23.2, 27, 'a');
println!("{}", tup.1); // 輸出:27
特殊的元組
當元組中只有一個元素時(即元組長度是 1),唯一的元素后面必須加上逗號:
let tup = (2,); // 逗號不能少,否則會提示你,單個值應該去掉小括號。這是避免將小括號當做計算的優(yōu)先級
自然,模式匹配解構元組時,也必須有逗號。
如果元組沒有元素呢?即空元組。看下面的代碼:
fn main() {
let result = test_tuple();
println!("{:?}", result);
}
fn test_tuple() {
println!("test empty tuple");
}
你猜打印 result 是啥?
擦,竟然是 (),即空元組。而且 Rust 給它專門取了一個名字:單元類型(unit type),也就是說,() 叫單元類型,它有一個唯一值:空元組 ()。而且,因為沒有任何元素,Rust 將其歸為變量類型。
還嫌 Rust 不夠復雜嗎?就叫空元組不行嗎?非得搞一個單元類型,這么奇怪的類型。。。
為了避免復雜性,我覺得大家將其理解為空元組即可。至于為什么這里會返回空元組,在函數(shù)部分會講解。
注意:() 是不占空間的,這和 Go 中的空結構體類似。
02 數(shù)組
Rust 中的數(shù)組和 Go 中的類似,是不可變的,由元素類型和長度確定,且長度必須是編譯期常量。Rust 中,數(shù)組類型標記為 [T; size]。數(shù)組字面值使用 [] 表示:
let a = [1, 2, 3, 4];
同樣會進行類型推斷(包括長度)(這里推斷出 a 的類型是 [i32; 4]),也可以顯示進行類型注解:
let a: [i8; 4] = [1, 2, 3, 4];
相比較而言,Rust 創(chuàng)建數(shù)組比 Go 簡單,它和 PHP 這樣的動態(tài)語言類似。在 Go 中一般這樣創(chuàng)建數(shù)組:
// Go 語言
a := [...]int{1, 2, 3, 4}
也就是說,Go 中創(chuàng)建數(shù)組是,類型信息不能少,沒法跟 Rust 一樣進行類型推斷。
除了上面的初始化方法,Rust 中還可以這樣簡單的初始化:
let a = [-1; 4]; // 4 個元素都是 -1
Rust 變量必須初始化后才能使用,而 Go 語言中,變量會有默認值。所以,Go 中可以簡單的定義一個數(shù)組,然后使用默認的初始值。如:
// Go 語言
var a [4]int // a 的值是:[0 0 0 0]
此外,Rust 中數(shù)組總是分配在棧中的,因此可以認為數(shù)組是「值類型」,和 Go 一樣,我們不應該直接傳遞數(shù)組,而應該和 Go 一樣,使用 slice。
03 切片(slice)
Rust 中的切片和 Go 中的切片意思一樣,表示對數(shù)組部分元素的引用。但和 Go 不同的是,Rust 的切片沒有容量的概念,只有一個指向數(shù)據(jù)的指針和切片的長度。Rust 中切片的類型標記為 &[T],即對數(shù)組進行引用(&)就是切片。
Go 語言中有直接創(chuàng)建切片的語法(比如 make),但 Rust 中沒有,它必須依賴數(shù)組或 Vec(以后講解),通過引用來創(chuàng)建。
let xs = [1, 2, 3, 4, 5];
let slice = &xs;
既然切片是數(shù)組元素的片段引用,那如何引用部分片段呢?
在 Go 中是這么做的:
var arr = [...]int{1, 2, 3, 4}
var slice1 = arr[:] // 結果是 [1 2 3 4],全部元素
var slice2 = arr[1:3] // 結果是 [2 3]
var slice3 = arr[:3] // 結果是 [1 2 3]
var slice4 = arr[1:] // 結果是 [2 3 4]
而在 Rust 中是這么做的:(結果和上面一樣)
let arr = [1, 2, 3, 4];
let slice1 = &arr[..];
let slice2 = &arr[1..3];
let slice3 = &arr[..3];
let slice4 = &arr[1..];
看到不同了嗎?
Rust 中生成切片,需要引用(&); Go 中使用 :來引用片段;而 Rust 使用..;
相同的點是,都可以省略起始或終止位置,或都省略。
關于
..以后還會講到
切片類型的方法(也適用于數(shù)組)
在 Rust 中,一切類型都有實現(xiàn)一些 trait,包括上一節(jié)的標量類型(用面向對象來講,一切皆對象)。現(xiàn)在先不探討 trait,著重看看 len 方法。具體參考標準庫文檔:https://doc.rust-lang.org/std/primitive.slice.html。
1)len:計算長度
數(shù)組或切片有一個 len() 方法可以計算長度。
pub const fn len(&self) -> usize
// 具體使用
let arr = [1, 2, 3];
assert_eq!(arr.len(), 3); // assert_eq 和 println 一樣,是一個宏,用來斷言
而 Go 語言中,使用 len(arr) 的形式,len 是內置函數(shù)。
不過,關于 len 還有一些細小的點。看下面的 Go 代碼,你覺得有問題嗎?
var arr = [...]int{1, 2, 3, 4}
var slice = arr[:]
var arr2 [len(arr)]int
var arr3 [len(slice)]int
在 Go 中,要求數(shù)組長度要求是編譯期常量。len(arr) 是編譯期常量,而 len(slice) 卻不是,因為 slice 的長度是可變的。所以,以上代碼 arr2 正確,arr3 編譯錯誤。
那 Rust 中是怎么樣的呢?
let arr = [1, 2, 3, 4];
let slice = &arr[..];
let arr2 = [0;arr.len()];
let arr3 = [0;slice.len()];
arr2 和 arr3 都編譯錯誤。arr3 錯誤可以理解,為什么 arr2 也不行呢?
根據(jù)編譯器提示,怎么修改 arr2 就可以了:
const ARR:[i32; 4] = [1, 2, 3, 4];
let arr2 = [0; ARR.len()];
也就是說必須是數(shù)組常量。。。但數(shù)組本身不就是不可變的嗎?非得定義成常量,多此一舉?據(jù)說,Rust 有可能將數(shù)組改成可變的。。。有了切片,為啥還要把數(shù)組搞這么復雜?!
2)其他方法
is_empty:判斷數(shù)組或切片是否為空 first:獲取第一個元素 last:獲取最后一個元素 。。。
first 和 last 有什么用?為啥不直接通過下標獲取?
last 的存在,使得我們不需要先調用 len 獲取長度來間接獲取最后一個元素。 而 first 的存在,使得我們不需要先判斷是否為空。
不過,因為存在數(shù)組或切片為空的情況,因此 first 和 last 返回的都是 Opiton 類型。關于該類型后續(xù)再講。
04 小結
我們用兩篇講解了 Rust 中的數(shù)據(jù)類型,同時和 Go 的數(shù)據(jù)類型進行了對比。但 Rust 中的數(shù)據(jù)類型不止這些,還有其他類型,我們以后再講,包括通過標準庫定義的數(shù)據(jù)類型。
再強調一次,本系列教程的目標是讓大家學習盡可能不被勸退,因此有些特別復雜但我認為可以不用的,就不會介紹。關于 Rust 中的 primitive type 可以在標準庫文檔找到,以及每個類型的方法。https://doc.rust-lang.org/std/index.html#primitives。
我是 polarisxu,北大碩士畢業(yè),曾在 360 等知名互聯(lián)網(wǎng)公司工作,10多年技術研發(fā)與架構經(jīng)驗!2012 年接觸 Go 語言并創(chuàng)建了 Go 語言中文網(wǎng)!著有《Go語言編程之旅》、開源圖書《Go語言標準庫》等。
堅持輸出技術(包括 Go、Rust 等技術)、職場心得和創(chuàng)業(yè)感悟!歡迎關注「polarisxu」一起成長!也歡迎加我微信好友交流:gopherstudio
