喜歡的話別忘了點贊、收藏加關注哦,對接下來的教程有興趣的可以關注專欄。謝謝喵!(=・ω・=)
題外話:泛型的概念非常非常非常重要!!!整個第10章全都是Rust的重難點!!!
10.2.1. 什么是泛型
泛型的主要功能是提高代碼的復用能力,適用于處理重復代碼的問題,也可以說是實現數據與算法的分離。
泛型是具體類型或其它屬性的抽象代替。 它的意思是你寫代碼時寫的泛型代碼并不是最終的代碼,而是一種模版,里面有一些“占位符”。
編譯器在編譯時會把“占位符”替換為具體的類型。 還是看個例子:
fn largest<T>(list:&[T]) -> T {
//......
}
這個函數的定義就使用了泛型類型參數,這個T
就是所謂的“占位符”,寫代碼時這個T
可以是任意的數據類型,但是在編譯時編譯器會根據具體的使用把T
替換為具體的類型,這個過程叫單態化。
這個T
叫做泛型的類型參數。其實可以使用任意合法的標識符來作為它的類型參數的名,但是按慣例通常是使用大寫的T
(代表Type)。其實在選擇泛型類型參數名的時候,它的名稱是很短的,通常一個字母就夠了。如果你實在要寫長一點,使用駝峰命名規范即可。
10.2.2. 函數定義中的泛型
當使用泛型來定義一個函數的時候,需要將泛型的類型參數放在函數的簽名里。而泛型的類型參數通常是用于指定參數和返回的類型。
以上一篇文章的代碼為例,使用泛型稍作修改:
fn largest<T>(list: &[T]) -> T{ let mut largest = list[0]; for &item in list{ if item > largest{ largest = item; } } largest
}
整個函數的定義可以這么理解:函數largest
擁有泛型的類型參數T
,它接收切片作為參數,切片內的元素為T
,而這個元素返回值的類型也是T
。
嘗試編譯一下,輸出:
error[E0369]: binary operation `>` cannot be applied to type `T`--> src/main.rs:4:17|
4 | if item > largest{| ---- ^ ------- T| || T|
help: consider restricting type parameter `T`|
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> T{| ++++++++++++++++++++++
這里先不講原因和修改的方法,只需要知道使用泛型參數大概是這么個寫法就對了。后面的文章會講如何指定特定的trait。
10.2.3. struct定義中的泛型
結構體里定義的泛型類型參數主要是用在它的字段里,看個例子:
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 };
}
在結構體的名字后面呢加上<>
,在里面寫泛型參數的名稱,而這個泛型類型就可以應用于這個結構體下的每個字段。
在main
函數里實現了這個結構體的實例化,integer
里的兩個字段是兩個i32
,float
里的兩個字段是兩個f64
,因為結構體在聲明時x
和y
的類型都是T
,所以實例化的x
和y
的類型也得是一個類型,兩者的類型得保持一致。
那如果我想要x
和y
是兩種不同的類型呢?很簡單,聲明兩個泛型類型就可以:
struct Point<T, U> { x: T, y: U,
} fn main() { let integer = Point { x: 5, y: 1.0 }; let float = Point { x: 1.0, y: 40 };
}
這個時候實例化的x
和y
就可以是不同的類型,當然也可以是一樣的類型。
需要注意的是,雖然可以使用多個泛型類型參數,但是,太多的泛型會使得可讀性下降,通常這意味著代碼需要重組為更多的小單元。
10.2.4. enum定義中的泛型
和結構體差不多,枚舉中使用泛型類型參數主要是用在變體中華,可以讓枚舉的變體持有泛型數據類型,比如說最常見的Option<T>
和Result<T, E>
。
看個例子:
enum Option<T> { Some(T), None,
} enum Result<T, E> { Ok(T), Err(E),
}
Option
枚舉中Some(T)
也就是Some
這個變體持有T
類型的值,而None
這個變體表示不持有任何值。而正是因為Option
枚舉使用了泛型,所以無論這個可能存在的值是什么類型的,都可以使用Option<T>
來表示- 同樣的,枚舉的類型參數也可以使用多個泛型類型參數,比如說
Result
這個枚舉就使用了T
和E
,在變體Ok
里存儲的是T
,Err
存儲的是E
10.2.5. 在方法定義中的泛型
方法可以附在枚舉或是結構體上,既然枚舉和結構體都可以使用泛型參數,那方法自然也可以,如下例:
struct Point<T> { x: T, y: T,
} impl<T> Point<T> { fn x(&self) -> &T { &self.x }
}
方法x
相當于一個getter,而針對Poinnt<T>
這個結構來實現方法的時候需要在impl
關鍵字的后面加上<T>
。這樣寫就表示它是針對泛型T
而不是針對某個具體的類型來實現的。
當然,如果是根據具體的類型來實現方法就不需要了:
impl Point<i32> { fn x1(&self) -> &i32 { &self.x }
}
而x1
這個方法就只有在Point<i32>
這個具體的類型上才有,而其他Point<T>
的類型就沒有這個方法,類比C++的特化和偏特化。
還有一點需要注意,結構體里的泛型類型參數可以和方法的泛型類型參數不同。看個例子:
struct Point<T, U> { x: T, y: U,
} impl<T, U> Point<T, U> { fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> { 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);
}
針對Point<T, U>
實現了方法mixup
,mixup
有兩個泛型類型參數,一個叫V
,一個叫W
,方法的兩個類型參數和Point
的兩個類型參數是不一樣的,當然具類型也有可能是一樣的。mixup
的第二個參數是other
,它的類型也是Point
,但這個Point
不一定和self
所指向的Point
的數據類型是一樣的,所以需要另起2個新的泛型類型參數。再看看返回類型,是Point<T, W>
,T
來自Point<T, U>
,W
來自Point<V, W>
。
看下main
函數,首先聲明了p1
,它的兩個字段都是i32
;然后又聲明了p2
,它的兩個字段分別是&str
(字符串切片)和char
(用''
代表是單個字符)。接著使用了mixup
這個函數,p1
對應的是Point<T, U>
,p2
對應的是Point<V, W>
,又根據各自的字段的類型可以推斷出T
是i32
,U
是i32
,V
是String
,W
是char
。mixup
返回類型是Point<T, W>
,具體到這個例子中就是Point<i32, char>
。
輸出:
p3.x = 5, p3.y = c
10.2.6. 泛型代碼的性能
使用泛型的代碼和使用具體類型的代碼的運行速度是一樣的。Rust在編譯時會執行單態化,將泛型類型替換為具體的類型,這樣在執行的時候就省去了類型替換的過程。
舉個例子:
fn main() {let integer = Some(5);let float = Some(5.0)
}
這里integer
是Option<i32>
,float
是Option<f64>
,在編譯的時候編譯器會把Option<T>
展開為Option_i32
和Option_f64
:
enum Option_i32 {Some(i32),None,
}enum Option_f64 {Some(f64),None,
}
也就是把Option<T>
這個泛型定義替換為了兩個具體類型的定義。
單態后的main
函數也變成了這樣:
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);
}