概述
特征(trait)是rust中的概念,類似于其他語言中的接口(interface)。特征定義了一個可以被共享的行為,只要實現了特征,你就能使用該行為。
如果不同的類型具有相同的行為,那么我們就可以定義一個特征,然后為這些類型實現該特征。定義特征是把一些方法組合在一起,目的是定義一個實現某些目標所必需的行為的集合。例如,我們現在有圓形和長方形兩個結構體,它們都可以擁有周長,面積。因此我們可以定義被共享的行為,只要實現了特征就可以使用。
pub trait Figure { // 為幾何圖形定義名為Figure的特征
fn girth(&self) -> u64; // 計算周長
fn area(&self) -> u64; // 計算面積
}
這里使用 trait 關鍵字來聲明一個特征,Figure 是特征名。在大括號中定義了該特征的所有方法,在這個例子中有兩個方法,分別是fn girth(&self) -> u64;和fn area(&self) -> u64;,特征只定義行為看起來是什么樣的,而不定義行為具體是怎么樣的。因此,我們只定義特征方法的簽名,而不進行實現,此時方法簽名結尾是 ;,而不是一個 {}。
接下來,每一個實現這個特征的類型都需要具體實現該特征的相應方法,編譯器也會確保任何實現 Figure 特征的類型都擁有與fn girth(&self) -> u64;和fn area(&self) -> u64;簽名的定義完全一致的方法。
Rust語言中的trait是非常重要的概念。在Rust中,trait這一個概念承 擔了多種職責。很類似Go中的interface,但trait職責遠比interface更多。trait中可以包含:函數、常量、類型等。
1,成員方法
我們在特質中定義了一個成員方法,代碼如下:
trait Shape {
fn area(&self) -> f64;
}
所有的trait中都有一個隱藏的類型Self(大寫S),代表當前這個實 現了此trait的具體類型。
trait中定義的函數,也可以稱作關聯函數 (associated function)。
函數的第一個參數如果是Self相關的類型,且 命名為self(小寫s),這個參數可以被稱為“receiver”(接收者)。
具有 receiver參數的函數,我們稱為“方法”(method),可以通過變量實例使 用小數點來調用。
沒有receiver參數的函數,我們稱為“靜態函 數”(static function),可以通過類型加雙冒號::的方式來調用。在 Rust中,函數和方法沒有本質區別。
Rust中Self(大寫S)和self(小寫s)都是關鍵字,大寫S的是類型名,小寫s的是變量名。請大家一定注意區分。
self參數同樣也可以指定類型,當然這個類型是有限制的,必須是包裝在Self類型之上的類型。
對于第一個self參數,常見的類型有self:Self、self:&Self、self:&mut Self等類型。
對于以上這些類型,Rust提供了一種簡化的寫法,我們可 以將參數簡寫為self、&self、&mut self。self參數只能用在第一個參數的 位置。
請注意“變量self”和“類型Self”的大小寫不同。比如:
trait T {
fn method1(self: Self);
fn method2(self: &Self);
fn method3(self: &mut Self);
}trait T {
fn method1(self);
fn method2(&self);
fn method3(&mut self);
}
我們可以為某些具體類型實現(impl)這個Shape trait。假如我們有一個結構體類型Circle,它實現了這個trait,代碼如下:
trait Shape {
fn area(&self) -> f64;
}struct Circle {radius: f64,
}
impl Shape for Circle {// Self 類型就是 Circle// self 的類型是 &Self,即 &Circlefn area(&self) -> f64 {// 訪問成員變量,需要用 self.radiusstd::f64::consts::PI * self.radius * self.radius}
}
fn main() {let c = Circle { radius : 2f64};// 第一個參數名字是 self,可以使用小數點語法調用println!("The area is {}", c.area());
}
另外,針對一個類型,我們可以直接對它impl來增加成員方法,無 須trait名字。比如:
impl Circle {fn get_radius(&self) -> f64 { self.radius }
}
我們可以把這段代碼看作是為Circle類型impl了一個匿名的trait。用這種方式定義的方法叫作這個類型的“內在方法”(inherent methods)。
trait中可以包含方法的默認實現。如果這個方法在trait中已經有了 方法體,那么在針對具體類型實現的時候,就可以選擇不用重寫。
當然,如果需要針對特殊類型作特殊處理,也可以選擇重新實現 來“override”默認的實現方式。比如,在標準庫中,迭代器Iterator這個 trait中就包含了十多個方法,但是,其中只有fn next(&mut self)- >OptionSelf::Item是沒有默認實現的。
其他的方法均有其默認實 現,在實現迭代器的時候只需挑選需要重寫的方法來實現即可。
self參數甚至可以是Box指針類型self:Box。另外,目前Rust 設計組也在考慮讓self變量的類型放得更寬,允許更多的自定義類型作為receiver,比如MyType。看下面的代碼:
trait Shape {fn area(self: Box<Self>) -> f64;
}struct Circle {radius: f64,
}impl Shape for Circle {// Self 類型就是 Circle// self 的類型是 Box<Self>,即 Box<Circle>fn area(self : Box<Self>) -> f64 {// 訪問成員變量,需要用 self.radiusstd::f64::consts::PI * self.radius * self.radius}
}fn main() {let c = Circle { radius : 2f64};// 編譯錯誤// c.area();let b = Box::new(Circle {radius : 4f64});// 編譯正確b.area();
}
//impl的對象甚至可以是trait。示例如下:trait Shape {fn area(&self) -> f64;
}trait Round {fn get_radius(&self) -> f64;
}struct Circle {radius: f64,
}impl Round for Circle {fn get_radius(&self) -> f64 { self.radius }
}// 注意這里是
impl Trait for Trait impl Shape for Round { //為滿足T:Round的具體類型增加一個成員方法fn area(&self) -> f64 {std::f64::consts::PI * self.get_radius() * self.get_radius()}
}fn main() {let c = Circle { radius : 2f64};// 編譯錯誤// c.area();let b = Box::new(Circle {radius : 4f64}) as Box<Round>;// 編譯正確b.area();
}
impl Shape for Round和impl<T:Round>Shape for T是不一樣的。
在前一種寫法中,self是&Round類型,它是一個trait object,是胖指針。
而在后一種寫法中,self是&T類型,是具體類型。
前一種寫法是為trait object增加一個成員方法,而后一種寫法是為所有的滿足T:Round的具體類型增加一個成員方法。
所以上面的示例中, 我們只能構造一個trait object之后才能調用area()成員方法。
impl Shape for Round這種寫法確實是很讓初學者糾結的, Round既是trait又是type。在將來,trait object的語法會被要求加上dyn關鍵字。
2,靜態方法
沒有receiver參數的方法(第一個參數不是self參數的方法)稱作“靜態方法”。
靜態方法可以通過Type::FunctionName()的方式調用。
需要注意的是,即便我們的第一個參數是Self相關類型,只要變量名字不是self,就不能使用小數點的語法調用函數。
struct T(i32);
impl T {
// 這是一個靜態方法fn func(this: &Self) {println!{"value {}", this.0};}
}
fn main() {
let x = T(42);
// x.func(); 小數點方式調用是不合法的
T::func(&x);
}
在標準庫中就有一些這樣的例子。Box的一系列方法Box:: into_raw(b:Self)? ?Box::leak(b:Self),
以及Rc的一系列方法 Rc::try_unwrap(this:Self)Rc::downgrade(this:&Self),都是這種情況。
它們的receiver不是self關鍵字,這樣設計的目的是強制用戶 用Rc::downgrade(&obj)的形式調用,而禁止obj.downgrade()形 式的調用。
這樣源碼表達出來的意思更清晰,不會因為Rc里面的成員方法和T里面的成員方法重名而造成誤解問題。
trait中也可以定義靜態函數。下面以標準庫中的std::default:: Default trait為例,介紹靜態函數的相關用法:
pub trait Default {
fn default() -> Self;
}
上面這個trait中包含了一個default()函數,它是一個無參數的函 數,返回的類型是實現該trait的具體類型。Rust中沒有“構造函數”的念。Default trait實際上可以看作一個針對無參數構造函數的統一抽象.比如在標準庫中,Vec::default()就是一個普通的靜態函數。
impl<T> Default for Vec<T> {
fn default() -> Vec<T> {
Vec::new()
}
}
跟C++相比,在Rust中,定義靜態函數沒必要使用static關鍵字,因 為它把self參數顯式在參數列表中列出來了。
作為對比,C++里面成員 方法默認可以訪問this指針,因此它需要用static關鍵字來標記靜態方 法。
Rust不采取這個設計,主要原因是self參數的類型變化太多,不同寫法語義差別很大,選擇顯式聲明self參數更方便指定它的類型。
3,擴展方法
我們還可以利用trait給其他的類型添加成員方法,哪怕這個類型不 是我們自己寫的。比如,我們可以為內置類型i32添加一個方法:
trait Double {
fn double(&self) -> Self;
}
impl Double for i32 {
fn double(&self) -> i32 { *self * 2 }
}
fn main() {
// 可以像成員方法一樣調用
let x : i32 = 10.double();
println!("{}", x);
}
哪怕這個類型不是在當前 的項目中聲明的,我們依然可以為它增加一些成員方法。
但我們也不是隨隨便便就可以這么做的,Rust對此有一個規定:
在聲明trait和 impltraitl的時候,Rust規定CoherenceRule(一致性規則)或稱為OrphanRule(孤兒規則):
imp塊要么與trait的聲明在同一個crate中,要么與類型的聲明在同一個crate中。
這是有意的設計。如果我們在使用其他的crate的時候, 強行把它們“拉郎配”,是會制造出bug的。比如說,我們寫了一個程 序,引用了外部庫lib1和lib2,lib1中聲明了一個trait T,lib2中聲明了一 個struct S,我們不能在自己的程序中針對S實現T。這也意味著,上游開 發者在給別人寫庫的時候,尤其要注意,一些比較常見的標準庫中的 trait,如Display Debug ToString Default等,應該盡可能地提供好。否 則,使用這個庫的下游開發者是沒辦法幫我們把這些trait實現的。同理,如果是匿名impl,那么這個impl塊必須與類型本身存在于同一個crate中。
Rust是一種用戶可以對內存有精確控制能力的強類型語言。我們可 以自由指定一個變量是在棧里面,還是在堆里面,變量和指針也是不同 的類型。類型是有大小(Size)的。有些類型的大小是在編譯階段可以 確定的,有些類型的大小是編譯階段無法確定的。目前版本的Rust規 定,在函數參數傳遞、返回值傳遞等地方,都要求這個類型在編譯階段 有確定的大小。否則,編譯器就不知道該如何生成代碼了。 而trait本身既不是具體類型,也不是指針類型,它只是定義了針對 類型的、抽象的“約束”。不同的類型可以實現同一個trait,滿足同一個 trait的類型可能具有不同的大小。因此,trait在編譯階段沒有固定大小,目前我們不能直接使用trait作為實例變量、參數、返回值。比如:
let x: Shape = Circle::new(); // Shape 不能做局部變量的類型
fn use_shape(arg : Shape) {} // Shape 不能直接做參數的類型
fn ret_shape() -> Shape {} // Shape 不能直接做返回值的類型
這樣的寫法是錯誤的,請一定要記住。trait的大小在編譯階段是不固定的,需要寫成dynShape形式,即編譯的時候把不確定大小的東西通過胖指針來代替,而指針在編譯期是確定的。
4,完整函數調用方法
Fully Qualified Syntax提供一種無歧義的函數調用語法,允許程序員精確地指定想調用的是那個函數。以前也叫UFCS(universal function call syntax),也就是所謂的“通用函數調用語法”。這個語法可以允許使用類似的寫法精確調用任何方法,包括成員方法和靜態方法。其他一切 函數調用語法都是它的某種簡略形式。它的具體寫法為::item。示例如下:
trait Cook {
fn start(&self);
}
trait Wash {
fn start(&self);
}
struct Chef;
impl Cook for Chef {
fn start(&self) { println!("Cook::start");}
}
impl Wash for Chef {
fn start(&self) { println!("Wash::start");}
}
fn main() {
let me = Chef;
me.start(); //error,出現歧義,編譯其器不知道調用哪個方法
}//有必要使用完整的函數調用語法來進行方法調用
fn main() {
let me = Chef;
// 函數名字使用更完整的path來指定,同時,self參數需要顯式傳遞 <Cook>::start(&me);
<Chef as Wash>::start(&me);
}
由此我們也可以看到,所謂的“成員方法”也沒什么特殊之處,它跟 普通的靜態方法的唯一區別是,第一個參數是self,而這個self只是一個 普通的函數參數而已。只不過這種成員方法也可以通過變量加小數點的 方式調用。變量加小數點的調用方式在大部分情況下看起來更簡單更美 觀,完全可以視為一種語法糖。
需要注意的是,通過小數點語法調用方法調用,有一個“隱藏 著”的“取引用”步驟。雖然我們看起來源代碼長的是這個樣子 me.start(),但是大家心里要清楚,真正傳遞給start()方法的參數是 &me而不是me,這一步是編譯器自動幫我們做的。\color{red}不論這個方法接受 的self參數究竟是Self、&Self還是&mut Self,最終在源碼上,我們都是 統一的寫法:variable.method()。而如果用UFCS語法來調用這個方 法,我們就不能讓編譯器幫我們自動取引用了,必須手動寫清楚。下面用一個示例演示一下成員方法和普通函數其實沒什么本質區別。
struct T(usize);
impl T {
fn get1(&self) -> usize {self.0}
fn get2(&self) -> usize {self.0}
}
fn get3(t: &T) -> usize { t.0 }
fn check_type( _ : fn(&T)->usize ) {}
fn main() {
check_type(T::get1);
check_type(T::get2);
check_type(get3);
}
可以看到,get1、get2和get3都可以自動轉成fn(&T)→usize類型。
5,trait 約束和繼承
Rust的trait的另外一個大用處是,作為泛型約束使用。
未完待完善。。。