associated type
本集目標
學會在 trait 中定義 associated type(關聯型別),理解它和泛型參數的差別。
概念說明
第 18 集我們學了多參數 trait:trait Convert<T>。但有時候,型別參數不是「開放的」——一個型別只會有一種合理的實作。
問題:多參數 trait 太自由了
想像一個「容器」的 trait。容器裡面裝什麼型別的元素?用多參數 trait 的話:
trait Container<T> {
fn first(&self) -> Option<&T>;
}
fn main() {}
但這意味著同一個型別可以同時實作 Container<i32> 和 Container<String>——通常容器只會有一種元素型別。
associated type:一對一的關係
associated type 解決了這個問題:
trait Container {
type Item;
// 要使用 Self 的 associated type,用 Self::Type 的語法
fn first(&self) -> Option<&Self::Item>;
}
fn main() {}
type Item; 宣告了一個 associated type。實作的時候必須指定它是什麼:
trait Container {
type Item;
fn first(&self) -> Option<&Self::Item>;
}
struct NumberList {
data: Vec<i32>,
}
impl Container for NumberList {
type Item = i32;
fn first(&self) -> Option<&i32> {
self.data.first()
}
}
fn main() {}
當 Self(NumberList)和角括號裡的參數(這裡沒有)都確定了,Item 就唯一確定是 i32,不會有歧義。
和泛型參數的差別
你可以把 trait 想像成一個函數,它接受一些「輸入」然後決定一些「輸出」:
- 輸入(input):
Self(誰來實作這個trait)和角括號裡的型別參數(<T>)。 - 輸出(output):associated type(
type Item)。
輸入決定了輸出——當你確定了「誰」(Self)和「角括號裡的參數」,associated type 就唯一確定了。
舉例來說,Convert<T> 裡的 T 是輸入,所以同一個 Self 搭配不同的 T 可以有不同的實作:i32 可以同時實作 Convert<String> 和 Convert<(i32,)>。
但 Container 的 Item 是輸出。當你確定了 Self 是 NumberList,Item 就只能有一個答案——i32。
用哪個?如果「確定了所有 input 之後,這個型別就只有一個合理的答案」,把它放在 associated type(output)。如果「同一組 input 可以搭配多種不同答案」,把它放在角括號裡(input)。
Deref 也有 associated type
第 23 集學的 Deref trait 就用了 associated type:
trait Deref {
type Target;
fn deref(&self) -> &Self::Target;
}
fn main() {}
type Target 就是解參考所觸及的型別。
舉例來說,如果 p 是 Box<i32>,而我們寫:
fn main() {
let p = Box::new(10);
let n = *p;
}
Rust 大致上會把 *p 這部分當成:
use std::ops::Deref;
fn main() {
let p = Box::new(10);
let n = *p.deref();
}
deref 接收 &self,所以 p.deref() 會先借用這個智慧指標,再回傳一個指向內部值的參考:&Self::Target。最後,外面的 * 沿著這個參考走過去。
對 Box<i32> 來說,Self::Target 是 i32,所以 .deref() 回傳 &i32。因為 i32 是 Copy,let n = *p; 能產生出另一個 i32。
這跟 Container 的 type Item 是同樣的道理:一旦 Self 定為 Box<i32>,Target 就只有一個答案——i32。這正是 Target 用 associated type 而不是泛型參數的原因。
DerefMut 用的是同一個 Target
可變的版本是 DerefMut。簡化之後長這樣:
trait Deref {
type Target;
fn deref(&self) -> &Self::Target;
}
trait DerefMut: Deref {
fn deref_mut(&mut self) -> &mut Self::Target;
}
fn main() {}
DerefMut 沒有另外宣告一個 associated type,它用的就是 Deref 的 Self::Target。
這件事是有道理的:不可變解參考和可變解參考,必須觸及同一種內部值。如果 Box<String> 的 Target = String,那 .deref() 回傳 &String,.deref_mut() 就回傳 &mut String。
舉例來說:
fn main() {
let mut p = Box::new(String::from("hello"));
*p = String::from("world");
}
因為等號左邊是 *p,Rust 需要對內部值的可變存取。它大致上會把這部分當成:
use std::ops::DerefMut;
fn main() {
let mut p = Box::new(String::from("hello"));
*p.deref_mut() = String::from("world");
}
deref_mut 接收 &mut self,所以 p.deref_mut() 會先可變地借用這個智慧指標,再回傳一個指向內部值的可變參考:&mut Self::Target。最後,外面的 * 沿著這個可變參考走過去,賦值就能把內部的 String 換掉。
在 trait bound 中指定 associated type
你可以在 trait bound 裡指定 associated type 的具體型別:
fn print_first<C: Container<Item = i32>>(c: &C) { ... }
fn main() {}
Container<Item = i32> 表示「實作了 Container,而且 Item 是 i32」。
如果只是要限制 associated type 必須實作某個 trait,也可以直接把 trait bound 寫在 associated type 後面:
fn print_first<C: Container<Item: Display>>(c: &C) { ... }
fn main() {}
Item: Display 表示「Container 的 Item 必須實作 Display」。
範例程式碼
use std::fmt::Display;
// 用 associated type 定義容器 trait
trait Container {
type Item;
fn first(&self) -> Option<&Self::Item>;
fn last(&self) -> Option<&Self::Item>;
fn len(&self) -> usize;
}
struct NumberList {
data: Vec<i32>,
}
impl Container for NumberList {
type Item = i32; // 指定 associated type
fn first(&self) -> Option<&i32> {
self.data.first()
}
fn last(&self) -> Option<&i32> {
self.data.last()
}
fn len(&self) -> usize {
self.data.len()
}
}
struct WordList {
words: Vec<String>,
}
impl Container for WordList {
type Item = String; // 不同的型別,不同的 Item
fn first(&self) -> Option<&String> {
self.words.first()
}
fn last(&self) -> Option<&String> {
self.words.last()
}
fn len(&self) -> usize {
self.words.len()
}
}
// 在 trait bound 中用 associated type
fn print_first_item<C>(c: &C)
where
C: Container,
C::Item: Display,
{
match c.first() {
Some(item) => println!("第一個元素:{}", item),
None => println!("容器是空的"),
}
}
fn main() {
let nums = NumberList { data: vec![10, 20, 30] };
let words = WordList {
words: vec![
String::from("hello"),
String::from("world"),
],
};
println!("數字容器長度:{}", nums.len());
print_first_item(&nums);
println!("文字容器長度:{}", words.len());
print_first_item(&words);
// last
match nums.last() {
Some(n) => println!("最後一個數字:{}", n),
None => println!("空的"),
}
}
重點整理
type Item;在trait中定義 associated type。- 用
Self::Item的語法可以在trait定義中讀取Self的 associated type。 - 實作時用
type Item = i32;指定具體型別。 - input vs output:
Self和角括號參數是 input,associated type 是 output。input 決定 output。 Deref的type Target也是 associated type——Box<T>的Target = T,代表解參考觸及的是T。DerefMut用的是同一個Self::Target,回傳&mut Self::Target。- 在
traitbound 中用Container<Item = i32>指定 associated type。 - 在
traitbound 中也可以用Container<Item: Display>限制 associated type 必須實作某個trait。