Rust 從入門到精通08-trait
Rust 語言中,trait 是一個(gè)非常重要的概念,可以包含:函數(shù)、常量、類型等。
通俗一點(diǎn)理解,trait 以一種抽象的方式定義共享的行為,可以被認(rèn)為是一些語言的接口,但是與接口也有一定區(qū)別,下面會(huì)介紹。
1、成員方法
trait 中可以定義方法。
trait?Shape?{
????fn?area(&self)?->?f64;
}
我們?cè)谝粋€(gè)名為 Shape 的 trait 中定義了一個(gè)方法 area。
1.1 方法參數(shù)
看上面定義的 Shape,方法的參數(shù)是 &self。其實(shí)對(duì)于每個(gè) trait 都有一個(gè)隱藏的類型 Self(大寫的 S),代表實(shí)現(xiàn)此 trait 的具體類型。Rust 中 Self 和 self 都是關(guān)鍵字,大寫的Self是類型名,小寫的 self 是變量名。其實(shí) area(&self) 等價(jià)于 area(self : &Self),只不過 rust 提供了簡(jiǎn)化的寫法。下面幾種情況都是等價(jià)的。
trait?T?{
????fn?method1(self?:?Self);
????fn?method2(self?:?&Self);
????fn?method3(self?:?&mut?Self);
}
//等價(jià)于下面方法定義
trait?T?{
????fn?method1(self);
????fn?method2(&self);
????fn?method3(&mut?self);
}
1.2 調(diào)用實(shí)例
可以參考如下例子:
trait?Shape?{
????fn?area(&self)?->?f64;
}
struct?Circle?{
????radius?:?f64,
}
impl?Shape?for?Circle?{
????//?Self?的類型就是?Circle
????fn?area(self?:?&Self)?->?f64{
????????//?可以通過self.radius訪問成員變量
????????std::f64::consts::PI?*?self.radius?*?self.radius
????}
}
fn?main()?{
????let?circle?=?Circle{?radius?:?2f64};
????println!("The?area?is?{}",circle.area())
}
①、通過 self.成員變量 來訪問成員變量;
②、通過 實(shí)例.成員方法 來調(diào)用成員方法;
2、匿名 trait
impl?Circle?{
????fn?get_radius(&self)?->?f64?{
????????self.radius
????}
}
impl 關(guān)鍵字后面直接接類型,沒有 trait 的名字。
可以將上面代碼看成是為 Circle 實(shí)現(xiàn)了一個(gè)匿名的 trait。
3、 靜態(tài)方法
靜態(tài)方法:第一個(gè)參數(shù)不是 self 參數(shù)的方法。
impl?Circle?{
????//?普通方法
????fn?get_radius(&self)?->?f64?{
????????self.radius
????}
????//?靜態(tài)方法
????fn?get_area(this?:?&Self)?->f64?{
????????std::f64::consts::PI?*?this.radius?*?this.radius
????}
}
fn?main()?{
????let?c?=?Circle{?radius?:?2f64};
????//?調(diào)用普通方法
????println!("The?radius?is?{}",c.radius);
????//?調(diào)用靜態(tài)方法
????println!("The?area?is?{}",Circle::get_area(&c))
}
注意和普通方法的區(qū)別,參數(shù)命名不同,以及調(diào)用方式不同(普通方法是小數(shù) 實(shí)例.方法 ,靜態(tài)方法是 類型::方法 )。
靜態(tài)方法的調(diào)用可以 Type::FunctionName()。
4、擴(kuò)展方法
利用 trait 給其它類型添加方法。
比如我們給內(nèi)置類型 i32 添加一個(gè)方法:
//?擴(kuò)展方法
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?double?is?{}",x);//20
}
5、泛型約束
5.1 靜態(tài)分發(fā)
在編譯期通過單態(tài)化分別生成具體類型的實(shí)例,所以調(diào)用 trait 限定中的方法也都是運(yùn)行時(shí)零成本的,因?yàn)椴恍枰谶\(yùn)行時(shí)再 進(jìn)行方法查找 。
fn?main()?{
????fn?myPrint<T:?ToString>(v:?T)?{
????????v.to_string();
????}
????
????let?c?=?'a';
????let?s?=?String::from("hello");
????
????myPrint::<char>(c);
????myPrint::<String>(s);
}
等價(jià)于:
fn?myPrint(c:char){
????c.to_string();
}
fn?myPrint(str:String){
????str.to_string();
}
5.2 動(dòng)態(tài)分發(fā)
在運(yùn)行時(shí)查找相應(yīng)類型的方法 , 會(huì)帶來一定的運(yùn)行時(shí)開銷。
fn?dyn_Print(t:&ToString){
????????t.to_string();
}
let?c?=?'a';
let?s?=?String::from("hello");
????
dyn_Print(&c);
dyn_Print(&s)
5、一致性原則
一致性原則,也稱為孤兒原則:
Impl 塊要么與 trait 塊的聲明在同一個(gè) crate 中,要么與類型的聲明在同一個(gè) crate 中。
也就是說如果 trait 來自外部,而且類型也來自外部 crate,編譯器是不允許你為這個(gè)類型 impl 這個(gè) trait。它們當(dāng)中至少有一個(gè)是在當(dāng)前 crate 中定義的。
這也給我們提供了一個(gè)標(biāo)準(zhǔn):上游開發(fā)者在寫庫的時(shí)候,一些比較常用的標(biāo)準(zhǔn) trait,如 Display/Debug/ToString/Default 等,應(yīng)該盡可能的提供好。否則下游使用這個(gè)庫的開發(fā)者是沒法幫我們實(shí)現(xiàn)這些 trait 的。
6、trait 和 接口區(qū)別
開篇我們說為了便于理解 trait,可以想象為其它語言,比如Java中的接口。但是實(shí)際上他們還是有很大的區(qū)別的。
因?yàn)?rust 是一種用戶可以對(duì)內(nèi)存有著精確控制的強(qiáng)類型語言。在目前 Rust 版本中規(guī)定:
函數(shù)傳參類型,返回值類型等都是要在編譯期確定大小的。
而 trait 本身既不是具體類型,也不是指針類型,它只是定義了針對(duì)類型的、抽象的約束。不同的類型可以實(shí)現(xiàn)同一個(gè) trait,滿足同一個(gè) trait 的類型可能具有不同的大小。
所以 trait 在編譯階段沒有固定的大小,我們不能直接使用 trait 作為實(shí)例變量、參數(shù)以及返回值。
類似下面的寫法都是錯(cuò)誤的:
trait?Shape?{
????fn?area(&self)?->?f64;
}
impl?Circle?{
????//錯(cuò)誤1:?trait(Shape)不能做參數(shù)的類型
????fn?use_shape(arg?:?Shape){
????}
????//錯(cuò)誤2:?trait(Shape)不能做返回值的類型
????fn?ret_shape()?->?Shape{
????}
}
fn?main()?{
????//?錯(cuò)誤3:trait(Shape)不能做局部變量的類型
????let?x?:?Shape?=?Circle::new();
}
可以看到編譯器的錯(cuò)誤提示:

7、derive
Rust 標(biāo)準(zhǔn)庫內(nèi)部實(shí)現(xiàn)了一些邏輯較為固定的 trait,通過 derive 配置可以幫助我們自動(dòng) impl 某些 trait。
struct?Foo?{
????data?:?i32,
}
fn?main()?{
????let?v1?=?Foo{data?:?0};
????println!("{:?}",v1)
}
加上 Debug 的trait 實(shí)現(xiàn),便于格式化打印 struct。
#[derive(Debug)] 等價(jià)于 impl Debug for Foo {}
目前,Rust 支持的可以自動(dòng) derive 的 trait 有如下:
Copy,Clone,Default,Hash,Debug,PartialEq,Eq,PartialOrd,Ord,RustcEncodable,RustcDecodable,FromPrimitive,Send,Sync
8、標(biāo)準(zhǔn)庫中常見 trait
在介紹 derive 時(shí),我們說明了內(nèi)置的一些 trait,這都是標(biāo)準(zhǔn)庫中比較常見的 trait,下面我們分別介紹這些 trait 是干什么的。
8.1 Display 和 Debug
可以分別看下源碼定義:
【Display】
pub?trait?Display?{
????///?Formats?the?value?using?the?given?formatter.
????///
????///?#?Examples
????///
????///?```
????///?use?std::fmt;
????///
????///?struct?Position?{
????///?????longitude:?f32,
????///?????latitude:?f32,
????///?}
????///
????///?impl?fmt::Display?for?Position?{
????///?????fn?fmt(&self,?f:?&mut?fmt::Formatter<'_>)?->?fmt::Result?{
????///?????????write!(f,?"({},?{})",?self.longitude,?self.latitude)
????///?????}
????///?}
????///
????///?assert_eq!("(1.987,?2.983)",
????///????????????format!("{}",?Position?{?longitude:?1.987,?latitude:?2.983,?}));
????///?```
????
????fn?fmt(&self,?f:?&mut?Formatter<'_>)?->?Result;
}
【Debug】
pub?trait?Debug?{
????///?Formats?the?value?using?the?given?formatter.
????///
????///?#?Examples
????///
????///?```
????///?use?std::fmt;
????///
????///?struct?Position?{
????///?????longitude:?f32,
????///?????latitude:?f32,
????///?}
????///
????///?impl?fmt::Debug?for?Position?{
????///?????fn?fmt(&self,?f:?&mut?fmt::Formatter<'_>)?->?fmt::Result?{
????///?????????f.debug_tuple("")
????///??????????.field(&self.longitude)
????///??????????.field(&self.latitude)
????///??????????.finish()
????///?????}
????///?}
????///
????///?let?position?=?Position?{?longitude:?1.987,?latitude:?2.983?};
????///?assert_eq!(format!("{:?}",?position),?"(1.987,?2.983)");
????///
????///?assert_eq!(format!("{:#?}",?position),?"(
????///?????1.987,
????///?????2.983,
????///?)");
????///?```
????
????fn?fmt(&self,?f:?&mut?Formatter<'_>)?->?Result;
}
①、只有實(shí)現(xiàn)了 Display trait 的類型,才能夠用 {} 格式打印出來。
②、只有實(shí)現(xiàn)了 Debug trait 的類型,才能夠用{:?} {:#?} 格式打印出來。
這兩者區(qū)別如下:
1、Display 假定了這個(gè)類型可以用 utf-8 格式的字符串表示,它是準(zhǔn)備給最終用戶看的,并不是所有的類型都應(yīng)該或者能夠?qū)崿F(xiàn)這個(gè) trait。這個(gè) trait 的 fmt 應(yīng)該如何格式化字符串,完全取決于程序員自己,編譯器不提供自動(dòng) derive 的功能。
2、標(biāo)準(zhǔn)庫中還有一個(gè)常用 trait 叫作 std::string::ToString,對(duì)于所有實(shí)現(xiàn)了 Display trait 的類型,都自動(dòng)實(shí)現(xiàn)了這個(gè) ToString trait 。它包含了一個(gè)方法 to_string(&self) -> String。任何一個(gè)實(shí)現(xiàn)了 Display trait 的類型,我們都可以對(duì)它調(diào)用 to_string() 方法格式化出一個(gè)字符串。
3、Debug 則主要是為了調(diào)試使用,建議所有的作為 API 的“公開”類型都應(yīng)該實(shí)現(xiàn)這個(gè) trait,以方便調(diào)試。它打印出來的字符串不是以“美觀易讀”為標(biāo)準(zhǔn),編譯器提供了自動(dòng) derive 的功能。
Rust 從入門到精通08-trait的評(píng)論 (共 條)
