ch10-01-syntax.md
commit 6a23b2c7ffe392d1329855444a1b3a88e93982b6
我們可以使用泛型為像函數(shù)簽名或結(jié)構(gòu)體這樣的項創(chuàng)建定義,這樣它們就可以用于多種不同的具體數(shù)據(jù)類型。讓我們看看如何使用泛型定義函數(shù)、結(jié)構(gòu)體、枚舉和方法,然后我們將討論泛型如何影響代碼性能。
當使用泛型定義函數(shù)時,本來在函數(shù)簽名中指定參數(shù)和返回值的類型的地方,會改用泛型來表示。采用這種技術(shù),使得代碼適應(yīng)性更強,從而為函數(shù)的調(diào)用者提供更多的功能,同時也避免了代碼的重復(fù)。
回到 largest
函數(shù),示例 10-4 中展示了兩個函數(shù),它們的功能都是尋找 slice 中最大值。
文件名: src/main.rs
fn largest_i32(list: &[i32]) -> i32 {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
fn largest_char(list: &[char]) -> char {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest_i32(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest_char(&char_list);
println!("The largest char is {}", result);
}
示例 10-4:兩個函數(shù),不同點只是名稱和簽名類型
largest_i32
函數(shù)是從示例 10-3 中摘出來的,它用來尋找 slice 中最大的 i32
。largest_char
函數(shù)尋找 slice 中最大的 char
。因為兩者函數(shù)體的代碼是一樣的,我們可以定義一個函數(shù),再引進泛型參數(shù)來消除這種重復(fù)。
為了參數(shù)化新函數(shù)中的這些類型,我們也需要為類型參數(shù)取個名字,道理和給函數(shù)的形參起名一樣。任何標識符都可以作為類型參數(shù)的名字。這里選用 T
,因為傳統(tǒng)上來說,Rust 的參數(shù)名字都比較短,通常就只有一個字母,同時,Rust 類型名的命名規(guī)范是駱駝命名法(CamelCase)。T
作為 “type” 的縮寫是大部分 Rust 程序員的首選。
如果要在函數(shù)體中使用參數(shù),就必須在函數(shù)簽名中聲明它的名字,好讓編譯器知道這個名字指代的是什么。同理,當在函數(shù)簽名中使用一個類型參數(shù)時,必須在使用它之前就聲明它。為了定義泛型版本的 largest
函數(shù),類型參數(shù)聲明位于函數(shù)名稱與參數(shù)列表中間的尖括號 <>
中,像這樣:
fn largest<T>(list: &[T]) -> T {
可以這樣理解這個定義:函數(shù) largest
有泛型類型 T
。它有個參數(shù) list
,其類型是元素為 T
的 slice。largest
函數(shù)的返回值類型也是 T
。
示例 10-5 中的 largest
函數(shù)在它的簽名中使用了泛型,統(tǒng)一了兩個實現(xiàn)。該示例也展示了如何調(diào)用 largest
函數(shù),把 i32
值的 slice 或 char
值的 slice 傳給它。請注意這些代碼還不能編譯,不過稍后在本章會解決這個問題。
文件名: src/main.rs
fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}", result);
}
示例 10-5:一個使用泛型參數(shù)的 largest
函數(shù)定義,尚不能編譯
如果現(xiàn)在就編譯這個代碼,會出現(xiàn)如下錯誤:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `T`
--> src/main.rs:5:17
|
5 | if item > largest {
| ---- ^ ------- T
| |
| T
|
help: consider restricting type parameter `T`
|
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> T {
| ++++++++++++++++++++++
For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10` due to previous error
注釋中提到了 std::cmp::PartialOrd
,這是一個 trait。下一部分會講到 trait。不過簡單來說,這個錯誤表明 largest
的函數(shù)體不能適用于 T
的所有可能的類型。因為在函數(shù)體需要比較 T
類型的值,不過它只能用于我們知道如何排序的類型。為了開啟比較功能,標準庫中定義的 std::cmp::PartialOrd
trait 可以實現(xiàn)類型的比較功能(查看附錄 C 獲取該 trait 的更多信息)。
標準庫中定義的 std::cmp::PartialOrd
trait 可以實現(xiàn)類型的比較功能。在 “trait 作為參數(shù)” 部分會講解如何指定泛型實現(xiàn)特定的 trait,不過讓我們先探索其他使用泛型參數(shù)的方法。
同樣也可以用 <>
語法來定義結(jié)構(gòu)體,它包含一個或多個泛型參數(shù)類型字段。示例 10-6 展示了如何定義和使用一個可以存放任何類型的 x
和 y
坐標值的結(jié)構(gòu)體 Point
:
文件名: src/main.rs
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
}
示例 10-6:Point
結(jié)構(gòu)體存放了兩個 T
類型的值 x
和 y
其語法類似于函數(shù)定義中使用泛型。首先,必須在結(jié)構(gòu)體名稱后面的尖括號中聲明泛型參數(shù)的名稱。接著在結(jié)構(gòu)體定義中可以指定具體數(shù)據(jù)類型的位置使用泛型類型。
注意 Point<T>
的定義中只使用了一個泛型類型,這個定義表明結(jié)構(gòu)體 Point<T>
對于一些類型 T
是泛型的,而且字段 x
和 y
都是 相同類型的,無論它具體是何類型。如果嘗試創(chuàng)建一個有不同類型值的 Point<T>
的實例,像示例 10-7 中的代碼就不能編譯:
文件名: src/main.rs
struct Point<T> {
x: T,
y: T,
}
fn main() {
let wont_work = Point { x: 5, y: 4.0 };
}
示例 10-7:字段 x
和 y
的類型必須相同,因為他們都有相同的泛型類型 T
在這個例子中,當把整型值 5 賦值給 x
時,就告訴了編譯器這個 Point<T>
實例中的泛型 T
是整型的。接著指定 y
為 4.0,它被定義為與 x
相同類型,就會得到一個像這樣的類型不匹配錯誤:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0308]: mismatched types
--> src/main.rs:7:38
|
7 | let wont_work = Point { x: 5, y: 4.0 };
| ^^^ expected integer, found floating-point number
For more information about this error, try `rustc --explain E0308`.
error: could not compile `chapter10` due to previous error
如果想要定義一個 x
和 y
可以有不同類型且仍然是泛型的 Point
結(jié)構(gòu)體,我們可以使用多個泛型類型參數(shù)。在示例 10-8 中,我們修改 Point
的定義為擁有兩個泛型類型 T
和 U
。其中字段 x
是 T
類型的,而字段 y
是 U
類型的:
文件名: src/main.rs
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let both_integer = Point { x: 5, y: 10 };
let both_float = Point { x: 1.0, y: 4.0 };
let integer_and_float = Point { x: 5, y: 4.0 };
}
示例 10-8:使用兩個泛型的 Point
,這樣 x
和 y
可能是不同類型
現(xiàn)在所有這些 Point
實例都合法了!你可以在定義中使用任意多的泛型類型參數(shù),不過太多的話,代碼將難以閱讀和理解。當你的代碼中需要許多泛型類型時,它可能表明你的代碼需要重構(gòu),分解成更小的結(jié)構(gòu)。
和結(jié)構(gòu)體類似,枚舉也可以在成員中存放泛型數(shù)據(jù)類型。第六章我們曾用過標準庫提供的 Option<T>
枚舉,這里再回顧一下:
enum Option<T> {
Some(T),
None,
}
現(xiàn)在這個定義應(yīng)該更容易理解了。如你所見 Option<T>
是一個擁有泛型 T
的枚舉,它有兩個成員:Some
,它存放了一個類型 T
的值,和不存在任何值的None
。通過 Option<T>
枚舉可以表達有一個可能的值的抽象概念,同時因為 Option<T>
是泛型的,無論這個可能的值是什么類型都可以使用這個抽象。
枚舉也可以擁有多個泛型類型。第九章使用過的 Result
枚舉定義就是一個這樣的例子:
enum Result<T, E> {
Ok(T),
Err(E),
}
Result
枚舉有兩個泛型類型,T
和 E
。Result
有兩個成員:Ok
,它存放一個類型 T
的值,而 Err
則存放一個類型 E
的值。這個定義使得 Result
枚舉能很方便的表達任何可能成功(返回 T
類型的值)也可能失敗(返回 E
類型的值)的操作。實際上,這就是我們在示例 9-3 用來打開文件的方式:當成功打開文件的時候,T
對應(yīng)的是 std::fs::File
類型;而當打開文件出現(xiàn)問題時,E
的值則是 std::io::Error
類型。
當你意識到代碼中定義了多個結(jié)構(gòu)體或枚舉,它們不一樣的地方只是其中的值的類型的時候,不妨通過泛型類型來避免重復(fù)。
在為結(jié)構(gòu)體和枚舉實現(xiàn)方法時(像第五章那樣),一樣也可以用泛型。示例 10-9 中展示了示例 10-6 中定義的結(jié)構(gòu)體 Point<T>
,和在其上實現(xiàn)的名為 x
的方法。
文件名: src/main.rs
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("p.x = {}", p.x());
}
示例 10-9:在 Point<T>
結(jié)構(gòu)體上實現(xiàn)方法 x
,它返回 T
類型的字段 x
的引用
這里在 Point<T>
上定義了一個叫做 x
的方法來返回字段 x
中數(shù)據(jù)的引用:
注意必須在 impl
后面聲明 T
,這樣就可以在 Point<T>
上實現(xiàn)的方法中使用它了。在 impl
之后聲明泛型 T
,這樣 Rust 就知道 Point
的尖括號中的類型是泛型而不是具體類型。因為再次聲明了泛型,我們可以為泛型參數(shù)選擇一個與結(jié)構(gòu)體定義中聲明的泛型參數(shù)所不同的名稱,不過依照慣例使用了相同的名稱。impl
中編寫的方法聲明了泛型類型可以定位為任何類型的實例,不管最終替換泛型類型的是何具體類型。
另一個選擇是定義方法適用于某些有限制(constraint)的泛型類型。例如,可以選擇為 Point<f32>
實例實現(xiàn)方法,而不是為泛型 Point
實例。示例 10-10 展示了一個沒有在 impl
之后(的尖括號)聲明泛型的例子,這里使用了一個具體類型,f32
:
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
示例 10-10:構(gòu)建一個只用于擁有泛型參數(shù) T
的結(jié)構(gòu)體的具體類型的 impl
塊
這段代碼意味著 Point<f32>
類型會有一個方法 distance_from_origin
,而其他 T
不是 f32
類型的 Point<T>
實例則沒有定義此方法。這個方法計算點實例與坐標 (0.0, 0.0) 之間的距離,并使用了只能用于浮點型的數(shù)學(xué)運算符。
結(jié)構(gòu)體定義中的泛型類型參數(shù)并不總是與結(jié)構(gòu)體方法簽名中使用的泛型是同一類型。示例 10-11 中為 Point
結(jié)構(gòu)體使用了泛型類型 X1
和 Y1
,為 mixup
方法簽名使用了 X2
和 Y2
來使得示例更加清楚。這個方法用 self
的 Point
類型的 x
值(類型 X1
)和參數(shù)的 Point
類型的 y
值(類型 Y2
)來創(chuàng)建一個新 Point
類型的實例:
文件名: src/main.rs
struct Point<X1, Y1> {
x: X1,
y: Y1,
}
impl<X1, Y1> Point<X1, Y1> {
fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
Point {
x: self.x,
y: other.y,
}
}
}
fn main() {
let p1 = Point { x: 5, y: 10.4 };
let p2 = Point { x: "Hello", y: 'c' };
let p3 = p1.mixup(p2);
println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}
示例 10-11:方法使用了與結(jié)構(gòu)體定義中不同類型的泛型
在 main
函數(shù)中,定義了一個有 i32
類型的 x
(其值為 5
)和 f64
的 y
(其值為 10.4
)的 Point
。p2
則是一個有著字符串 slice 類型的 x
(其值為 "Hello"
)和 char
類型的 y
(其值為c
)的 Point
。在 p1
上以 p2
作為參數(shù)調(diào)用 mixup
會返回一個 p3
,它會有一個 i32
類型的 x
,因為 x
來自 p1
,并擁有一個 char
類型的 y
,因為 y
來自 p2
。println!
會打印出 p3.x = 5, p3.y = c
。
這個例子的目的是展示一些泛型通過 impl
聲明而另一些通過方法定義聲明的情況。這里泛型參數(shù) X1
和 Y1
聲明于 impl
之后,因為他們與結(jié)構(gòu)體定義相對應(yīng)。而泛型參數(shù) X2
和 Y2
聲明于 fn mixup
之后,因為他們只是相對于方法本身的。
在閱讀本部分內(nèi)容的同時,你可能會好奇使用泛型類型參數(shù)是否會有運行時消耗。好消息是:Rust 實現(xiàn)了泛型,使得使用泛型類型參數(shù)的代碼相比使用具體類型并沒有任何速度上的損失。
Rust 通過在編譯時進行泛型代碼的 單態(tài)化(monomorphization)來保證效率。單態(tài)化是一個通過填充編譯時使用的具體類型,將通用代碼轉(zhuǎn)換為特定代碼的過程。
編譯器所做的工作正好與示例 10-5 中我們創(chuàng)建泛型函數(shù)的步驟相反。編譯器尋找所有泛型代碼被調(diào)用的位置并使用泛型代碼針對具體類型生成代碼。
讓我們看看一個使用標準庫中 Option
枚舉的例子:
let integer = Some(5);
let float = Some(5.0);
當 Rust 編譯這些代碼的時候,它會進行單態(tài)化。編譯器會讀取傳遞給 Option<T>
的值并發(fā)現(xiàn)有兩種 Option<T>
:一個對應(yīng) i32
另一個對應(yīng) f64
。為此,它會將泛型定義 Option<T>
展開為 Option_i32
和 Option_f64
,接著將泛型定義替換為這兩個具體的定義。
編譯器生成的單態(tài)化版本的代碼看起來像這樣,并包含將泛型 Option<T>
替換為編譯器創(chuàng)建的具體定義后的用例代碼:
文件名: src/main.rs
enum Option_i32 {
Some(i32),
None,
}
enum Option_f64 {
Some(f64),
None,
}
fn main() {
let integer = Option_i32::Some(5);
let float = Option_f64::Some(5.0);
}
我們可以使用泛型來編寫不重復(fù)的代碼,而 Rust 將會為每一個實例編譯其特定類型的代碼。這意味著在使用泛型時沒有運行時開銷;當代碼運行,它的執(zhí)行效率就跟好像手寫每個具體定義的重復(fù)代碼一樣。這個單態(tài)化過程正是 Rust 泛型在運行時極其高效的原因。
更多建議: