Rust 勸退系列 05:複合數據類型

閱讀本文大概需要 8 分鐘。

大家好,我是站長 polarisxu。

這是 Rust 勸退系列的第 5 個教程,探討 Rust 中的複合數據類型(Compound types)。Rust 中有兩種原生的複合類型:元組(tuple)和數組(array),順帶介紹切片。

01 元組類型

Go 語言沒有元組類型,但多返回值有點類似元組(但還是有區別的哦)。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 語言中函數多返回值類似元組,在接收多返回值時,通過多個變量接收,比如:

// 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 一樣報錯,但會警告!

除了模式匹配解構,還可以使用類似訪問數組元素的方式訪問元組元素,只不過不是用[],而是用 . 加索引的方式(索引也是從 0 開始):

let tup = (23.2, 27, 'a');
println!("{}", tup.1); // 輸出:27

特殊的元組

當元組中只有一個元素時(即元組長度是 1),唯一的元素後面必須加上逗號:

let tup = (2,); // 逗號不能少,否則會提示你,單個值應該去掉小括號。這是避免將小括號當做計算的優先級

自然,模式匹配解構元組時,也必須有逗號。

如果元組沒有元素呢?即空元組。看下面的代碼:

fn main() {
    let result = test_tuple();
    println!("{:?}", result);
}

fn test_tuple() {
    println!("test empty tuple");
}

你猜打印 result 是啥?

擦,竟然是 (),即空元組。而且 Rust 給它專門取了一個名字:單元類型(unit type),也就是說,() 叫單元類型,它有一個唯一值:空元組 ()。而且,因爲沒有任何元素,Rust 將其歸爲變量類型。

還嫌 Rust 不夠複雜嗎?就叫空元組不行嗎?非得搞一個單元類型,這麼奇怪的類型。。。

爲了避免複雜性,我覺得大家將其理解爲空元組即可。至於爲什麼這裏會返回空元組,在函數部分會講解。

注意:() 是不佔空間的,這和 Go 中的空結構體類似。

02 數組

Rust 中的數組和 Go 中的類似,是不可變的,由元素類型和長度確定,且長度必須是編譯期常量。Rust 中,數組類型標記爲 [T; size]。數組字面值使用 [] 表示:

let a = [1, 2, 3, 4];

同樣會進行類型推斷(包括長度)(這裏推斷出 a 的類型是 [i32; 4]),也可以顯示進行類型註解:

let a: [i8; 4] = [1, 2, 3, 4];

相比較而言,Rust 創建數組比 Go 簡單,它和 PHP 這樣的動態語言類似。在 Go 中一般這樣創建數組:

// Go 語言
a := [...]int{1, 2, 3, 4}

也就是說,Go 中創建數組是,類型信息不能少,沒法跟 Rust 一樣進行類型推斷。

除了上面的初始化方法,Rust 中還可以這樣簡單的初始化:

let a = [-1; 4]; // 4 個元素都是 -1

Rust 變量必須初始化後才能使用,而 Go 語言中,變量會有默認值。所以,Go 中可以簡單的定義一個數組,然後使用默認的初始值。如:

// Go 語言
var a [4]int  // a 的值是:[0 0 0 0]

此外,Rust 中數組總是分配在棧中的,因此可以認爲數組是「值類型」,和 Go 一樣,我們不應該直接傳遞數組,而應該和 Go 一樣,使用 slice。

03 切片(slice)

Rust 中的切片和 Go 中的切片意思一樣,表示對數組部分元素的引用。但和 Go 不同的是,Rust 的切片沒有容量的概念,只有一個指向數據的指針和切片的長度。Rust 中切片的類型標記爲 &[T],即對數組進行引用(&)就是切片。

Go 語言中有直接創建切片的語法(比如 make),但 Rust 中沒有,它必須依賴數組或 Vec(以後講解),通過引用來創建。

let xs = [1, 2, 3, 4, 5];
let slice = &xs;

既然切片是數組元素的片段引用,那如何引用部分片段呢?

在 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 中,一切類型都有實現一些 trait,包括上一節的標量類型(用面向對象來講,一切皆對象)。現在先不探討 trait,着重看看 len 方法。具體參考標準庫文檔:https://doc.rust-lang.org/std/primitive.slice.html。

1)len:計算長度

數組或切片有一個 len() 方法可以計算長度。

pub const fn len(&self) -> usize

// 具體使用
let arr = [1, 2, 3];
assert_eq!(arr.len(), 3);  // assert_eq 和 println 一樣,是一個宏,用來斷言

而 Go 語言中,使用 len(arr) 的形式,len 是內置函數。

不過,關於 len 還有一些細小的點。看下面的 Go 代碼,你覺得有問題嗎?

var arr = [...]int{1, 2, 3, 4}
var slice = arr[:]

var arr2 [len(arr)]int
var arr3 [len(slice)]int

在 Go  中,要求數組長度要求是編譯期常量。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 也不行呢?

根據編譯器提示,怎麼修改 arr2 就可以了:

const ARR:[i32; 4] = [1, 2, 3, 4];
let arr2 = [0; ARR.len()];

也就是說必須是數組常量。。。但數組本身不就是不可變的嗎?非得定義成常量,多此一舉?據說,Rust 有可能將數組改成可變的。。。有了切片,爲啥還要把數組搞這麼複雜?!

2)其他方法

first 和 last 有什麼用?爲啥不直接通過下標獲取?

不過,因爲存在數組或切片爲空的情況,因此 first 和 last 返回的都是 Opiton 類型。關於該類型後續再講。

04 小結

我們用兩篇講解了 Rust 中的數據類型,同時和 Go 的數據類型進行了對比。但 Rust 中的數據類型不止這些,還有其他類型,我們以後再講,包括通過標準庫定義的數據類型。

再強調一次,本系列教程的目標是讓大家學習儘可能不被勸退,因此有些特別複雜但我認爲可以不用的,就不會介紹。關於 Rust 中的 primitive type 可以在標準庫文檔找到,以及每個類型的方法。https://doc.rust-lang.org/std/index.html#primitives。

我是 polarisxu,北大碩士畢業,曾在 360 等知名互聯網公司工作,10 多年技術研發與架構經驗!2012 年接觸 Go 語言並創建了 Go 語言中文網!著有《Go 語言編程之旅》、開源圖書《Go 語言標準庫》等。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/dtYnwOy3FkGjnOV7vshzvg