官术网_书友最值得收藏!

  • 深入淺出Rust
  • 范長(zhǎng)春
  • 8228字
  • 2018-11-08 15:09:12

第5章 trait

Rust語(yǔ)言中的trait是非常重要的概念。在Rust中,trait這一個(gè)概念承擔(dān)了多種職責(zé)。在中文里,trait可以翻譯為“特征”“特點(diǎn)”“特性”等。由于這些詞區(qū)分度并不明顯,在本書(shū)中一律不翻譯trait這個(gè)詞,以避免歧義。

trait中可以包含:函數(shù)、常量、類(lèi)型等。

5.1 成員方法

trait中可以定義函數(shù)。用例子來(lái)說(shuō)明,我們定義如下的trait:

    trait Shape {
        fn area(&self) -> f64;
    }

上面這個(gè)trait包含了一個(gè)方法,這個(gè)方法只有一個(gè)參數(shù),這個(gè)&self參數(shù)是什么意思呢?

所有的trait中都有一個(gè)隱藏的類(lèi)型Self(大寫(xiě)S),代表當(dāng)前這個(gè)實(shí)現(xiàn)了此trait的具體類(lèi)型。trait中定義的函數(shù),也可以稱(chēng)作關(guān)聯(lián)函數(shù)(associated function)。函數(shù)的第一個(gè)參數(shù)如果是Self相關(guān)的類(lèi)型,且命名為self(小寫(xiě)s),這個(gè)參數(shù)可以被稱(chēng)為“receiver”(接收者)。具有receiver參數(shù)的函數(shù),我們稱(chēng)為“方法”(method),可以通過(guò)變量實(shí)例使用小數(shù)點(diǎn)來(lái)調(diào)用。沒(méi)有receiver參數(shù)的函數(shù),我們稱(chēng)為“靜態(tài)函數(shù)”(static function),可以通過(guò)類(lèi)型加雙冒號(hào)::的方式來(lái)調(diào)用。在Rust中,函數(shù)和方法沒(méi)有本質(zhì)區(qū)別。

Rust中Self(大寫(xiě)S)和self(小寫(xiě)s)都是關(guān)鍵字,大寫(xiě)S的是類(lèi)型名,小寫(xiě)s的是變量名。請(qǐng)大家一定注意區(qū)分。self參數(shù)同樣也可以指定類(lèi)型,當(dāng)然這個(gè)類(lèi)型是有限制的,必須是包裝在Self類(lèi)型之上的類(lèi)型。對(duì)于第一個(gè)self參數(shù),常見(jiàn)的類(lèi)型有self :Self、self : &Self、self : &mut Self等類(lèi)型。對(duì)于以上這些類(lèi)型,Rust提供了一種簡(jiǎn)化的寫(xiě)法,我們可以將參數(shù)簡(jiǎn)寫(xiě)為self、&self、&mut self。self參數(shù)只能用在第一個(gè)參數(shù)的位置。請(qǐng)注意“變量self”和“類(lèi)型Self”的大小寫(xiě)不同。示例如下:

    trait T {
        fn method1(self: Self);
        fn method2(self: &Self);
        fn method3(self: &mut Self);
    }
    // 上下兩種寫(xiě)法是完全一樣的
    trait T {
        fn method1(self);
        fn method2(&self);
        fn method3(&mut self);
    }

所以,回到開(kāi)始定義的那個(gè)Shape trait,上面定義的這個(gè)area方法的參數(shù)的名字為self,它的類(lèi)型是&Self類(lèi)型。我們可以把上面這個(gè)方法的聲明看成:

    trait Shape {
        fn area(self: &Self) -> f64;
    }

我們可以為某些具體類(lèi)型實(shí)現(xiàn)(impl)這個(gè)trait。

假如我們有一個(gè)結(jié)構(gòu)體類(lèi)型Circle,它實(shí)現(xiàn)了這個(gè)trait,代碼如下:

    struct Circle {
        radius: f64,
    }
    impl Shape for Circle {
        // Self 類(lèi)型就是 Circle
        // self 的類(lèi)型是 &Self, &Circle
        fn area(&self) -> f64 {
            // 訪問(wèn)成員變量,需要用 self.radius
            std::f64::consts::PI * self.radius * self.radius
        }
    }
    fn main() {
        let c = Circle { radius : 2f64};
        // 第一個(gè)參數(shù)名字是 self,可以使用小數(shù)點(diǎn)語(yǔ)法調(diào)用
        println!("The area is {}", c.area());
    }

在上面的例子中可以看到,如果有一個(gè)Circle類(lèi)型的實(shí)例c,我們就可以用小數(shù)點(diǎn)調(diào)用函數(shù),c.area()。在方法內(nèi)部,我們可以通過(guò)self.radius的方式訪問(wèn)類(lèi)型的內(nèi)部成員。

另外,針對(duì)一個(gè)類(lèi)型,我們可以直接對(duì)它impl來(lái)增加成員方法,無(wú)須trait名字。比如:

    impl Circle {
        fn get_radius(&self) -> f64 { self.radius }
    }

我們可以把這段代碼看作是為Circle類(lèi)型impl了一個(gè)匿名的trait。用這種方式定義的方法叫作這個(gè)類(lèi)型的“內(nèi)在方法”(inherent methods)。

trait中可以包含方法的默認(rèn)實(shí)現(xiàn)。如果這個(gè)方法在trait中已經(jīng)有了方法體,那么在針對(duì)具體類(lèi)型實(shí)現(xiàn)的時(shí)候,就可以選擇不用重寫(xiě)。當(dāng)然,如果需要針對(duì)特殊類(lèi)型作特殊處理,也可以選擇重新實(shí)現(xiàn)來(lái)“override”默認(rèn)的實(shí)現(xiàn)方式。比如,在標(biāo)準(zhǔn)庫(kù)中,迭代器Iterator這個(gè)trait中就包含了十多個(gè)方法,但是,其中只有fn next(&mut self) ->Option<Self::Item>是沒(méi)有默認(rèn)實(shí)現(xiàn)的。其他的方法均有其默認(rèn)實(shí)現(xiàn),在實(shí)現(xiàn)迭代器的時(shí)候只需挑選需要重寫(xiě)的方法來(lái)實(shí)現(xiàn)即可。

self參數(shù)甚至可以是Box指針類(lèi)型self : Box<Self>。另外,目前Rust設(shè)計(jì)組也在考慮讓self變量的類(lèi)型放得更寬,允許更多的自定義類(lèi)型作為receiver,比如MyType<Self>。示例如下:

    trait Shape {
        fn area(self: Box<Self>) -> f64;
    }
    struct Circle {
        radius: f64,
    }
    impl Shape for Circle {
        // Self 類(lèi)型就是 Circle
        // self 的類(lèi)型是 Box<Self>, Box<Circle>
        fn area(self : Box<Self>) -> f64 {
            // 訪問(wèn)成員變量,需要用 self.radius
            std::f64::consts::PI * self.radius * self.radius
        }
    }
    fn main() {
        let c = Circle { radius : 2f64};
        // 編譯錯(cuò)誤
        // c.area();
        let b = Box::new(Circle {radius : 4f64});
        // 編譯正確
        b.area();
    }

impl的對(duì)象甚至可以是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 {
        fn area(&self) -> f64 {
            std::f64::consts::PI * self.get_radius() * self.get_radius()
        }
    }
    fn main() {
        let c = Circle { radius : 2f64};
        // 編譯錯(cuò)誤
        // c.area();
        let b = Box::new(Circle {radius : 4f64}) as Box<Round>;
        // 編譯正確
        b.area();
    }

注意這里的寫(xiě)法,impl Shape for Round和impl<T: Round> Shape for T是不一樣的。在前一種寫(xiě)法中,self是&Round類(lèi)型,它是一個(gè)trait object,是胖指針。而在后一種寫(xiě)法中,self是&T類(lèi)型,是具體類(lèi)型。前一種寫(xiě)法是為trait object增加一個(gè)成員方法,而后一種寫(xiě)法是為所有的滿(mǎn)足T: Round的具體類(lèi)型增加一個(gè)成員方法。所以上面的示例中,我們只能構(gòu)造一個(gè)trait object之后才能調(diào)用area()成員方法。trait object和“泛型”之間的區(qū)別請(qǐng)參考本書(shū)第三部分。

題外話,impl Shape for Round這種寫(xiě)法確實(shí)是很讓初學(xué)者糾結(jié)的,Round既是trait又是type。在將來(lái),trait object的語(yǔ)法會(huì)被要求加上dyn關(guān)鍵字,所以在Rust 2018 edition以后應(yīng)該寫(xiě)成impl Shape for dyn Round才合理。關(guān)于trait object的內(nèi)容,請(qǐng)參考本書(shū)第三部分第23章。

5.2 靜態(tài)方法

沒(méi)有receiver參數(shù)的方法(第一個(gè)參數(shù)不是self參數(shù)的方法)稱(chēng)作“靜態(tài)方法”。靜態(tài)方法可以通過(guò)Type::FunctionName()的方式調(diào)用。需要注意的是,即便我們的第一個(gè)參數(shù)是Self相關(guān)類(lèi)型,只要變量名字不是self,就不能使用小數(shù)點(diǎn)的語(yǔ)法調(diào)用函數(shù)。

    struct T(i32);
    impl T {
        // 這是一個(gè)靜態(tài)方法
        fn func(this: &Self) {
            println!{"value {}", this.0};
        }
    }
    fn main() {
        let x = T(42);
        // x.func(); 小數(shù)點(diǎn)方式調(diào)用是不合法的
        T::func(&x);
    }

在標(biāo)準(zhǔn)庫(kù)中就有一些這樣的例子。Box的一系列方法Box::into_raw(b: Self) Box::leak(b: Self),以及Rc的一系列方法Rc::try_unwrap(this: Self) Rc::downgrade(this: &Self),都是這種情況。它們的receiver不是self關(guān)鍵字,這樣設(shè)計(jì)的目的是強(qiáng)制用戶(hù)用Rc::downgrade(&obj)的形式調(diào)用,而禁止obj. downgrade()形式的調(diào)用。這樣源碼表達(dá)出來(lái)的意思更清晰,不會(huì)因?yàn)镽c<T>里面的成員方法和T里面的成員方法重名而造成誤解問(wèn)題(這又涉及Deref trait的內(nèi)容,讀者可以把第16章讀完再回看這一段)。

trait中也可以定義靜態(tài)函數(shù)。下面以標(biāo)準(zhǔn)庫(kù)中的std::default::Default trait為例,介紹靜態(tài)函數(shù)的相關(guān)用法:

    pub trait Default {
        fn default() -> Self;
    }

上面這個(gè)trait中包含了一個(gè)default()函數(shù),它是一個(gè)無(wú)參數(shù)的函數(shù),返回的類(lèi)型是實(shí)現(xiàn)該trait的具體類(lèi)型。Rust中沒(méi)有“構(gòu)造函數(shù)”的概念。Default trait實(shí)際上可以看作一個(gè)針對(duì)無(wú)參數(shù)構(gòu)造函數(shù)的統(tǒng)一抽象。

比如在標(biāo)準(zhǔn)庫(kù)中,Vec::default()就是一個(gè)普通的靜態(tài)函數(shù)。

    // 這里用到了“泛型”,請(qǐng)參閱第21
    impl<T> Default for Vec<T> {
        fn default() -> Vec<T> {
            Vec::new()
        }
    }

跟C++相比,在Rust中,定義靜態(tài)函數(shù)沒(méi)必要使用static關(guān)鍵字,因?yàn)樗裺elf參數(shù)顯式在參數(shù)列表中列出來(lái)了。作為對(duì)比,C++里面成員方法默認(rèn)可以訪問(wèn)this指針,因此它需要用static關(guān)鍵字來(lái)標(biāo)記靜態(tài)方法。Rust不采取這個(gè)設(shè)計(jì),主要原因是self參數(shù)的類(lèi)型變化太多,不同寫(xiě)法語(yǔ)義差別很大,選擇顯式聲明self參數(shù)更方便指定它的類(lèi)型。

5.3 擴(kuò)展方法

我們還可以利用trait給其他的類(lèi)型添加成員方法,哪怕這個(gè)類(lèi)型不是我們自己寫(xiě)的。比如,我們可以為內(nèi)置類(lèi)型i32添加一個(gè)方法:

    trait Double {
        fn double(&self) -> Self;
    }
    impl Double for i32 {
        fn double(&self) -> i32 { *self * 2 }
    }
    fn main() {
        // 可以像成員方法一樣調(diào)用
        let x : i32 = 10.double();
        println! ("{}", x);
    }

這個(gè)功能就像C#里面的“擴(kuò)展方法”一樣。哪怕這個(gè)類(lèi)型不是在當(dāng)前的項(xiàng)目中聲明的,我們依然可以為它增加一些成員方法。但我們也不是隨隨便便就可以這么做的,Rust對(duì)此有一個(gè)規(guī)定。

在聲明trait和impl trait的時(shí)候,Rust規(guī)定了一個(gè)Coherence Rule(一致性規(guī)則)或稱(chēng)為Orphan Rule(孤兒規(guī)則):impl塊要么與trait的聲明在同一個(gè)的crate中,要么與類(lèi)型的聲明在同一個(gè)crate中。

也就是說(shuō),如果trait來(lái)自于外部crate,而且類(lèi)型也來(lái)自于外部crate,編譯器不允許你為這個(gè)類(lèi)型impl這個(gè)trait。它們之中必須至少有一個(gè)是在當(dāng)前crate中定義的。因?yàn)樵谄渌腸rate中,一個(gè)類(lèi)型沒(méi)有實(shí)現(xiàn)一個(gè)trait,很可能是有意的設(shè)計(jì)。如果我們?cè)谑褂闷渌腸rate的時(shí)候,強(qiáng)行把它們“拉郎配”,是會(huì)制造出bug的。比如說(shuō),我們寫(xiě)了一個(gè)程序,引用了外部庫(kù)lib1和lib2, lib1中聲明了一個(gè)trait T, lib2中聲明了一個(gè)struct S,我們不能在自己的程序中針對(duì)S實(shí)現(xiàn)T。這也意味著,上游開(kāi)發(fā)者在給別人寫(xiě)庫(kù)的時(shí)候,尤其要注意,一些比較常見(jiàn)的標(biāo)準(zhǔn)庫(kù)中的trait,如Display Debug ToString Default等,應(yīng)該盡可能地提供好。否則,使用這個(gè)庫(kù)的下游開(kāi)發(fā)者是沒(méi)辦法幫我們把這些trait實(shí)現(xiàn)的。

同理,如果是匿名impl,那么這個(gè)impl塊必須與類(lèi)型本身存在于同一個(gè)crate中。

更多關(guān)于“一致性規(guī)則”的解釋?zhuān)梢詤⒁?jiàn)編譯器的詳細(xì)錯(cuò)誤說(shuō)明:

    rustc --explain E0117
    rustc --explain E0210

當(dāng)類(lèi)型和trait涉及泛型參數(shù)的時(shí)候,一致性規(guī)則實(shí)際上是很復(fù)雜的,用戶(hù)如果需要了解所有的細(xì)節(jié),還需要參考對(duì)應(yīng)的RFC文檔。

許多初學(xué)者會(huì)用自帶GC的語(yǔ)言中的“Interface”、抽象基類(lèi)來(lái)理解trait這個(gè)概念,但是實(shí)際上它們有很大的不同。

Rust是一種用戶(hù)可以對(duì)內(nèi)存有精確控制能力的強(qiáng)類(lèi)型語(yǔ)言。我們可以自由指定一個(gè)變量是在棧里面,還是在堆里面,變量和指針也是不同的類(lèi)型。類(lèi)型是有大小(Size)的。有些類(lèi)型的大小是在編譯階段可以確定的,有些類(lèi)型的大小是編譯階段無(wú)法確定的。目前版本的Rust規(guī)定,在函數(shù)參數(shù)傳遞、返回值傳遞等地方,都要求這個(gè)類(lèi)型在編譯階段有確定的大小。否則,編譯器就不知道該如何生成代碼了。

而trait本身既不是具體類(lèi)型,也不是指針類(lèi)型,它只是定義了針對(duì)類(lèi)型的、抽象的“約束”。不同的類(lèi)型可以實(shí)現(xiàn)同一個(gè)trait,滿(mǎn)足同一個(gè)trait的類(lèi)型可能具有不同的大小。因此,trait在編譯階段沒(méi)有固定大小,目前我們不能直接使用trait作為實(shí)例變量、參數(shù)、返回值。

有一些初學(xué)者特別喜歡寫(xiě)這樣的代碼:

    let x: Shape = Circle::new(); // Shape 不能做局部變量的類(lèi)型
    fn use_shape(arg : Shape) {}  // Shape 不能直接做參數(shù)的類(lèi)型
    fn ret_shape() -> Shape {}     // Shape 不能直接做返回值的類(lèi)型

這樣的寫(xiě)法是錯(cuò)誤的。請(qǐng)一定要記住,trait的大小在編譯階段是不固定的。那怎樣寫(xiě)才是對(duì)的呢?后面我們講到泛型的時(shí)候再說(shuō)。

5.4 完整函數(shù)調(diào)用語(yǔ)法

Fully Qualified Syntax提供一種無(wú)歧義的函數(shù)調(diào)用語(yǔ)法,允許程序員精確地指定想調(diào)用的是那個(gè)函數(shù)。以前也叫UFCS(universal function call syntax),也就是所謂的“通用函數(shù)調(diào)用語(yǔ)法”。這個(gè)語(yǔ)法可以允許使用類(lèi)似的寫(xiě)法精確調(diào)用任何方法,包括成員方法和靜態(tài)方法。其他一切函數(shù)調(diào)用語(yǔ)法都是它的某種簡(jiǎn)略形式。它的具體寫(xiě)法為<T as TraitName>::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();
    }

我們定義了兩個(gè)trait,它們的start()函數(shù)有同樣方法簽名。

如果一個(gè)類(lèi)型同時(shí)實(shí)現(xiàn)了這兩個(gè)trait,那么如果我們使用variable.start()這樣的語(yǔ)法執(zhí)行方法調(diào)用的話,就會(huì)出現(xiàn)歧義,編譯器不知道你具體想調(diào)用哪個(gè)方法,編譯錯(cuò)誤信息為“multiple applicable items in scope”。

這時(shí)候,我們就有必要使用完整的函數(shù)調(diào)用語(yǔ)法來(lái)進(jìn)行方法調(diào)用,只有這樣寫(xiě),才能清晰明白且無(wú)歧義地表達(dá)清楚期望調(diào)用的是哪個(gè)函數(shù):

    fn main() {
        let me = Chef;
    // 函數(shù)名字使用更完整的path來(lái)指定,同時(shí),self參數(shù)需要顯式傳遞
        <Cook>::start(&me);
        <Chef as Wash>::start(&me);
    }

由此我們也可以看到,所謂的“成員方法”也沒(méi)什么特殊之處,它跟普通的靜態(tài)方法的唯一區(qū)別是,第一個(gè)參數(shù)是self,而這個(gè)self只是一個(gè)普通的函數(shù)參數(shù)而已。只不過(guò)這種成員方法也可以通過(guò)變量加小數(shù)點(diǎn)的方式調(diào)用。變量加小數(shù)點(diǎn)的調(diào)用方式在大部分情況下看起來(lái)更簡(jiǎn)單更美觀,完全可以視為一種語(yǔ)法糖。

需要注意的是,通過(guò)小數(shù)點(diǎn)語(yǔ)法調(diào)用方法調(diào)用,有一個(gè)“隱藏著”的“取引用”步驟。雖然我們看起來(lái)源代碼長(zhǎng)的是這個(gè)樣子me.start(),但是大家心里要清楚,真正傳遞給start()方法的參數(shù)是&me而不是me,這一步是編譯器自動(dòng)幫我們做的。不論這個(gè)方法接受的self參數(shù)究竟是Self、&Self還是&mut Self,最終在源碼上,我們都是統(tǒng)一的寫(xiě)法:variable.method()。而如果用UFCS語(yǔ)法來(lái)調(diào)用這個(gè)方法,我們就不能讓編譯器幫我們自動(dòng)取引用了,必須手動(dòng)寫(xiě)清楚。

下面用一個(gè)示例演示一下成員方法和普通函數(shù)其實(shí)沒(méi)什么本質(zhì)區(qū)別。

    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都可以自動(dòng)轉(zhuǎn)成fn(&T)→usize類(lèi)型。

5.5 trait約束和繼承

Rust的trait的另外一個(gè)大用處是,作為泛型約束使用。關(guān)于泛型,本書(shū)第三部分還會(huì)詳細(xì)解釋。下面用一個(gè)簡(jiǎn)單示例演示一下trait如何作為泛型約束使用:

    use std::fmt::Debug;
    fn my_print<T : Debug>(x: T) {
        println!("The value is {:? }.", x);
    }
    fn main() {
        my_print("China");
        my_print(41_i32);
        my_print(true);
        my_print(['a', 'b', 'c'])
    }

上面這段代碼中,my_print函數(shù)引入了一個(gè)泛型參數(shù)T,所以它的參數(shù)不是一個(gè)具體類(lèi)型,而是一組類(lèi)型。冒號(hào)后面加trait名字,就是這個(gè)泛型參數(shù)的約束條件。它要求這個(gè)T類(lèi)型實(shí)現(xiàn)Debug這個(gè)trait。這是因?yàn)槲覀冊(cè)诤瘮?shù)體內(nèi),用到了println!格式化打印,而且用了{(lán):? }這樣的格式控制符,它要求類(lèi)型滿(mǎn)足Debug的約束,否則編譯不過(guò)。

在調(diào)用的時(shí)候,凡是滿(mǎn)足Debug約束的類(lèi)型都可以是這個(gè)函數(shù)的參數(shù),所以我們可以看到以上四種調(diào)用都是可以編譯通過(guò)的。假如我們自定義一個(gè)類(lèi)型,而它沒(méi)有實(shí)現(xiàn)Debug trait,我們就會(huì)發(fā)現(xiàn),用這個(gè)類(lèi)型作為my_print的參數(shù)的話,編譯就會(huì)報(bào)錯(cuò)。

所以,泛型約束既是對(duì)實(shí)現(xiàn)部分的約束,也是對(duì)調(diào)用部分的約束。

泛型約束還有另外一種寫(xiě)法,即where子句。示例如下:

    fn my_print<T>(x: T) where T: Debug {
        println!("The value is {:? }.", x);
    }

對(duì)于這種簡(jiǎn)單的情況,兩種寫(xiě)法都可以。但是在某些復(fù)雜的情況下,泛型約束只有where子句可以表達(dá),泛型參數(shù)后面直接加冒號(hào)的寫(xiě)法表達(dá)不出來(lái),比如涉及關(guān)聯(lián)類(lèi)型的時(shí)候,請(qǐng)參見(jiàn)第21章。

trait允許繼承。類(lèi)似下面這樣:

    trait Base { ... }
    trait Derived : Base { ... }

這表示Derived trait繼承了Base trait。它表達(dá)的意思是,滿(mǎn)足Derived的類(lèi)型,必然也滿(mǎn)足Base trait。所以,我們?cè)卺槍?duì)一個(gè)具體類(lèi)型impl Derived的時(shí)候,編譯器也會(huì)要求我們同時(shí)impl Base。示例如下:

    trait Base {}
    trait Derived : Base {}
    struct T;
    impl Derived for T {}
    fn main() {
    }

編譯,出現(xiàn)錯(cuò)誤,提示信息為:

    --> test.rs:7:6
      |
    7 | impl Derived for T {}
      |      ^^^^^^^ the trait `Base` is not implemented for `T`

我們?cè)偌由弦痪?/p>

    impl Base for T {}

編譯器就不再報(bào)錯(cuò)了。

實(shí)際上,在編譯器的眼中,trait Derived : Base {}等同于trait Derived where Self : Base {}。這兩種寫(xiě)法沒(méi)有本質(zhì)上的區(qū)別,都是給Derived這個(gè)trait加了一個(gè)約束條件,即實(shí)現(xiàn)Derived trait的具體類(lèi)型,也必須滿(mǎn)足Base trait的約束。

在標(biāo)準(zhǔn)庫(kù)中,很多trait之間都有繼承關(guān)系,比如:

    trait Eq: PartialEq<Self> {}
    trait Copy: Clone {}
    trait Ord: Eq + PartialOrd<Self> {}
    trait FnMut<Args>: FnOnce<Args> {}
    trait Fn<Args>: FnMut<Args> {}

讀完本書(shū)后,讀者應(yīng)該能夠理解這些trait是用來(lái)做什么的,以及為什么這些trait之間會(huì)有這樣的繼承關(guān)系。

5.6 Derive

Rust里面為類(lèi)型impl某些trait的時(shí)候,邏輯是非常機(jī)械化的。為許多類(lèi)型重復(fù)而單調(diào)地impl某些trait,是非常枯燥的事情。為此,Rust提供了一個(gè)特殊的attribute,它可以幫我們自動(dòng)impl某些trait。示例如下:

    #[derive(Copy, Clone, Default, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
    struct Foo {
        data : i32
    }
    fn main() {
        let v1 = Foo { data : 0 };
        let v2 = v1;
        println!("{:? }", v2);
    }

如上所示,它的語(yǔ)法是,在你希望impl trait的類(lèi)型前面寫(xiě)#[derive(…)],括號(hào)里面是你希望impl的trait的名字。這樣寫(xiě)了之后,編譯器就幫你自動(dòng)加上了impl塊,類(lèi)似這樣:

    impl Copy for Foo { ... }
    impl Clone for Foo { ... }
    impl Default for Foo { ... }
    impl Debug for Foo { ... }
    impl Hash for Foo { ... }
    impl PartialEq for Foo { ... }
    ......

這些trait都是標(biāo)準(zhǔn)庫(kù)內(nèi)部的較特殊的trait,它們可能包含有成員方法,但是成員方法的邏輯有一個(gè)簡(jiǎn)單而一致的“模板”可以使用,編譯器就機(jī)械化地重復(fù)這個(gè)模板,幫我們實(shí)現(xiàn)這個(gè)默認(rèn)邏輯。當(dāng)然我們也可以手動(dòng)實(shí)現(xiàn)。

目前,Rust支持的可以自動(dòng)derive的trait有以下這些:

    Debug    Clone    Copy    Hash    RustcEncodable    RustcDecodable    PartialEq    Eq
        ParialOrd    Ord    Default    FromPrimitive    Send    Sync

5.7 trait別名

跟type alias類(lèi)似的,trait也可以起別名(trait alias)。假如在某些場(chǎng)景下,我們有一個(gè)比較復(fù)雜的trait:

    pub trait Service {
        type Request;
        type Response;
        type Error;
        type Future: Future<Item=Self::Response, Error=Self::Error>;
        fn call(&self, req: Self::Request) -> Self::Future;
    }

每次使用這個(gè)trait的時(shí)候都需要攜帶一堆的關(guān)聯(lián)類(lèi)型參數(shù)。為了避免這樣的麻煩,在已經(jīng)確定了關(guān)聯(lián)類(lèi)型的場(chǎng)景下,我們可以為它取一個(gè)別名,比如:

    trait HttpService = Service<Request = http::Request,
            Response = http::Response,
            Error = http::Error>;

5.8 標(biāo)準(zhǔn)庫(kù)中常見(jiàn)的trait簡(jiǎn)介

標(biāo)準(zhǔn)庫(kù)中有很多很有用的trait,本節(jié)挑幾個(gè)特別常見(jiàn)的給大家介紹一下。

5.8.1 Display和Debug

這兩個(gè)trait在標(biāo)準(zhǔn)庫(kù)中的定義是這樣的:

    // std::fmt::Display
    pub trait Display {
        fn fmt(&self, f: &mut Formatter) -> Result<(), Error>;
    }
    // std::fmt::Debug
    pub trait Debug {
        fn fmt(&self, f: &mut Formatter) -> Result<(), Error>;
    }

它們的主要用處就是用在類(lèi)似println!這樣的地方:

    use std::fmt::{Display, Formatter, Error};
    #[derive(Debug)]
    struct T {
        field1: i32,
        field2: i32,
    }
    impl Display for T {
        fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
            write! (f, "{{ field1:{}, field2:{} }}", self.field1, self.field2)
        }
    }
    fn main() {
        let var = T { field1: 1, field2: 2 };
        println!("{}", var);
        println!("{:? }", var);
        println!("{:#? }", var);
    }

只有實(shí)現(xiàn)了Display trait的類(lèi)型,才能用{}格式控制打印出來(lái);只有實(shí)現(xiàn)了Debug trait的類(lèi)型,才能用{:? } {:#? }格式控制打印出來(lái)。它們之間更多的區(qū)別如下。

? Display假定了這個(gè)類(lèi)型可以用utf-8格式的字符串表示,它是準(zhǔn)備給最終用戶(hù)看的,并不是所有類(lèi)型都應(yīng)該或者能夠?qū)崿F(xiàn)這個(gè)trait。這個(gè)trait的fmt應(yīng)該如何格式化字符串,完全取決于程序員自己,編譯器不提供自動(dòng)derive的功能。

? 標(biāo)準(zhǔn)庫(kù)中還有一個(gè)常用trait叫作std::string::ToString,對(duì)于所有實(shí)現(xiàn)了Display trait的類(lèi)型,都自動(dòng)實(shí)現(xiàn)了這個(gè)ToString trait。它包含了一個(gè)方法to_string(&self) -> String。任何一個(gè)實(shí)現(xiàn)了Display trait的類(lèi)型,我們都可以對(duì)它調(diào)用to_string()方法格式化出一個(gè)字符串。

? Debug則是主要為了調(diào)試使用,建議所有的作為API的“公開(kāi)”類(lèi)型都應(yīng)該實(shí)現(xiàn)這個(gè)trait,以方便調(diào)試。它打印出來(lái)的字符串不是以“美觀易讀”為標(biāo)準(zhǔn),編譯器提供了自動(dòng)derive的功能。

5.8.2 PartialOrd / Ord / PartialEq / Eq

在前文中講解浮點(diǎn)類(lèi)型的時(shí)候提到,因?yàn)镹aN的存在,浮點(diǎn)數(shù)是不具備“total order(全序關(guān)系)”的。在這里,我們?cè)敿?xì)討論一下什么是全序、什么是偏序。Rust標(biāo)準(zhǔn)庫(kù)中有如下解釋。

對(duì)于集合X中的元素a, b, c,

? 如果a < b則一定有! (a > b);反之,若a > b,則一定有!(a < b),稱(chēng)為反對(duì)稱(chēng)性。

? 如果a < b且b < c則a < c,稱(chēng)為傳遞性。

? 對(duì)于X中的所有元素,都存在a < b或a > b或者a == b,三者必居其一,稱(chēng)為完全性。

如果集合X中的元素只具備上述前兩條特征,則稱(chēng)X是“偏序”。同時(shí)具備以上所有特征,則稱(chēng)X是“全序”。

從以上定義可以看出,浮點(diǎn)數(shù)不具備“全序”特征,因?yàn)楦↑c(diǎn)數(shù)中特殊的值NaN不滿(mǎn)足完全性。這就導(dǎo)致了一個(gè)問(wèn)題:浮點(diǎn)數(shù)無(wú)法排序。對(duì)于任意一個(gè)不是NaN的數(shù)和NaN之間做比較,無(wú)法分出先后關(guān)系。示例如下:

    fn main() {
        let nan = std::f32::NAN;
        let x = 1.0f32;
        println!("{}", nan < x);
        println!("{}", nan > x);
        println!("{}", nan == x);
    }

以上不論是NaN < x, NaN > x還是NaN == x,結(jié)果都是false。這是IEEE754標(biāo)準(zhǔn)中規(guī)定的行為。

因此,Rust設(shè)計(jì)了兩個(gè)trait來(lái)描述這樣的狀態(tài):一個(gè)是std::cmp::PartialOrd,表示“偏序”,一個(gè)是std::cmp::Ord,表示“全序”。它們的對(duì)外接口是這樣定義的:

    pub trait PartialOrd<Rhs: ? Sized = Self>: PartialEq<Rhs> {
        fn partial_cmp(&self, other: &Rhs) -> Option<Ordering>;
        fn lt(&self, other: &Rhs) -> bool { //... }
        fn le(&self, other: &Rhs) -> bool { //... }
        fn gt(&self, other: &Rhs) -> bool { //... }
        fn ge(&self, other: &Rhs) -> bool { //... }
    }
    pub trait Ord: Eq + PartialOrd<Self> {
        fn cmp(&self, other: &Self) -> Ordering;
    }

從以上代碼可以看出,partial_cmp函數(shù)的返回值類(lèi)型是Option<Ordering>。只有Ord trait里面的cmp函數(shù)才能返回一個(gè)確定的Ordering。f32和f64類(lèi)型都只實(shí)現(xiàn)了PartialOrd,而沒(méi)有實(shí)現(xiàn)Ord。

因此,如果我們寫(xiě)出下面的代碼,編譯器是會(huì)報(bào)錯(cuò)的:

    let int_vec = [1_i32, 2, 3];
    let biggest_int = int_vec.iter().max();
    let float_vec = [1.0_f32, 2.0, 3.0];
    let biggest_float = float_vec.iter().max();

對(duì)整數(shù)i32類(lèi)型的數(shù)組求最大值是沒(méi)問(wèn)題的,但是對(duì)浮點(diǎn)數(shù)類(lèi)型的數(shù)組求最大值是不對(duì)的,編譯錯(cuò)誤為:

    the trait 'core::cmp::Ord' is not implemented for the type 'f32'

筆者認(rèn)為,這個(gè)設(shè)計(jì)是優(yōu)點(diǎn),而不是缺點(diǎn),它讓我們盡可能地在更早的階段發(fā)現(xiàn)錯(cuò)誤,而不是留到運(yùn)行時(shí)再去debug。假如說(shuō)編譯器無(wú)法靜態(tài)檢查出這樣的問(wèn)題,那么就可能發(fā)生下面的情況,以Python為例:

    Python 3.4.2 (default, Oct  82014, 10:45:20)
    [GCC 4.9.1] on linux
    Type "help", "copyright", "credits" or "license" for more information.
    >>> v = [1.0, float("nan")]
    >>> max(v)
    1.0
    >>> v = [float("nan"), 1.0]
    >>> max(v)
    nan

上面這個(gè)示例意味著,如果數(shù)組v中有NaN,對(duì)它求最大值,跟數(shù)組內(nèi)部元素的排列順序有關(guān)。

Rust中的PartialOrd trait實(shí)際上就是C++20中即將加入的three-way comparison運(yùn)算符<=>。

同理,PartialEq和Eq兩個(gè)trait也就可以理解了,它們的作用是比較相等關(guān)系,與排序關(guān)系非常類(lèi)似。

5.8.3 Sized

Sized trait是Rust中一個(gè)非常重要的trait,它的定義如下:

    #[lang = "sized"]
    #[rustc_on_unimplemented  =  "`{Self}`  does  not  have  a  constant  size  known  at
compile-time"]
    #[fundamental] // for Default, for example, which requires that `[T]: ! Default`
be evaluatable
    pub trait Sized {
        // Empty.
    }

這個(gè)trait定義在std::marker模塊中,它沒(méi)有任何的成員方法。它有#[lang ="sized"]屬性,說(shuō)明它與普通trait不同,編譯器對(duì)它有特殊的處理。用戶(hù)也不能針對(duì)自己的類(lèi)型impl這個(gè)trait。一個(gè)類(lèi)型是否滿(mǎn)足Sized約束是完全由編譯器推導(dǎo)的,用戶(hù)無(wú)權(quán)指定。

我們知道,在C/C++這一類(lèi)的語(yǔ)言中,大部分變量、參數(shù)、返回值都應(yīng)該是編譯階段固定大小的。在Rust中,但凡編譯階段能確定大小的類(lèi)型,都滿(mǎn)足Sized約束。那還有什么類(lèi)型是不滿(mǎn)足Sized約束的呢?比如C語(yǔ)言里的不定長(zhǎng)數(shù)組(Variable-length Array)。不定長(zhǎng)數(shù)組的長(zhǎng)度在編譯階段是未知的,是在執(zhí)行階段才確定下來(lái)的。Rust里面也有類(lèi)似的類(lèi)型[T]。在Rust中VLA類(lèi)型已經(jīng)通過(guò)了RFC設(shè)計(jì),只是暫時(shí)還沒(méi)有實(shí)現(xiàn)而已。不定長(zhǎng)類(lèi)型在使用的時(shí)候有一些限制,比如不能用它作為函數(shù)的返回類(lèi)型,而必須將這個(gè)類(lèi)型藏到指針背后才可以。但它作為一個(gè)類(lèi)型,依然是有意義的,我們可以為它添加成員方法,用它實(shí)例化泛型參數(shù),等等。

Rust中對(duì)于動(dòng)態(tài)大小類(lèi)型專(zhuān)門(mén)有一個(gè)名詞Dynamic Sized Type。我們后面將會(huì)看到的[T], str以及dyn Trait都是DST。

5.8.4 Default

Rust里面并沒(méi)有C++里面的“構(gòu)造函數(shù)”的概念。大家可以看到,它只提供了類(lèi)似C語(yǔ)言的各種復(fù)合類(lèi)型各自的初始化語(yǔ)法。主要原因在于,相比普通函數(shù),構(gòu)造函數(shù)本身并沒(méi)有提供什么額外的抽象能力。所以Rust里面推薦使用普通的靜態(tài)函數(shù)作為類(lèi)型的“構(gòu)造器”。比如,常見(jiàn)的標(biāo)準(zhǔn)庫(kù)中提供的字符串類(lèi)型String,它包含的可以構(gòu)造新的String的方法不完全列舉都有這么多:

    fn new() -> String
    fn with_capacity(capacity: usize) -> String
    fn from_utf8(vec: Vec<u8>) -> Result<String, FromUtf8Error>
    fn from_utf8_lossy<'a>(v: &'a [u8]) -> Cow<'a, str>
    fn from_utf16(v: &[u16]) -> Result<String, FromUtf16Error>
    fn from_utf16_lossy(v: &[u16]) -> String
    unsafe fn from_raw_parts(buf: *mut u8, length: usize, capacity: usize) -> String
    unsafe fn from_utf8_unchecked(bytes: Vec<u8>) -> String

這還不算Default::default()、From::from(s: &'a str)、FromIte r a t-or::from_iter <I: IntoIterator<Item=char>>(iter: I)、Iter a t-or::collect等相對(duì)復(fù)雜的構(gòu)造方法。這些方法接受的參數(shù)各異,錯(cuò)誤處理方式也各異,強(qiáng)行將它們統(tǒng)一到同名字的構(gòu)造函數(shù)重載中不是什么好主意(況且Rust堅(jiān)決反對(duì)ad hoc式的函數(shù)重載)。

不過(guò),對(duì)于那種無(wú)參數(shù)、無(wú)錯(cuò)誤處理的簡(jiǎn)單情況,標(biāo)準(zhǔn)庫(kù)中提供了Default trait來(lái)做這個(gè)統(tǒng)一抽象。這個(gè)trait的簽名如下:

    trait Default {
        fn default() -> Self;
    }

它只包含一個(gè)“靜態(tài)函數(shù)”default()返回Self類(lèi)型。標(biāo)準(zhǔn)庫(kù)中很多類(lèi)型都實(shí)現(xiàn)了這個(gè)trait,它相當(dāng)于提供了一個(gè)類(lèi)型的默認(rèn)值。

在Rust中,單詞new并不是一個(gè)關(guān)鍵字。所以我們可以看到,很多類(lèi)型中都使用了new作為函數(shù)名,用于命名那種最常用的創(chuàng)建新對(duì)象的情況。因?yàn)檫@些new函數(shù)差別甚大,所以并沒(méi)有一個(gè)trait來(lái)對(duì)這些new函數(shù)做一個(gè)統(tǒng)一抽象。

5.9 總結(jié)

本章對(duì)trait這個(gè)概念做了基本的介紹。除了上面介紹的之外,trait還有許多用處:

? trait本身可以攜帶泛型參數(shù);

? trait可以用在泛型參數(shù)的約束中;

? trait可以為一組類(lèi)型impl,也可以單獨(dú)為某一個(gè)具體類(lèi)型impl,而且它們可以同時(shí)存在;

? trait可以為某個(gè)trait impl,而不是為某個(gè)具體類(lèi)型impl;

? trait可以包含關(guān)聯(lián)類(lèi)型,而且還可以包含類(lèi)型構(gòu)造器,實(shí)現(xiàn)高階類(lèi)型的某些功能;

? trait可以實(shí)現(xiàn)泛型代碼的靜態(tài)分派,也可以通過(guò)trait object實(shí)現(xiàn)動(dòng)態(tài)分派;

? trait可以不包含任何方法,用于給類(lèi)型做標(biāo)簽(marker),以此來(lái)描述類(lèi)型的一些重要特性;

? trait可以包含常量。

trait這個(gè)概念在Rust語(yǔ)言中扮演了非常重要的角色,承擔(dān)了各種各樣的功能,在寫(xiě)代碼的時(shí)候會(huì)經(jīng)常用到。本章還遠(yuǎn)沒(méi)有把trait相關(guān)的知識(shí)講解完整,更多關(guān)于trait的內(nèi)容,請(qǐng)參閱本書(shū)后文中與泛型、trait object線程安全有關(guān)的章節(jié)。

主站蜘蛛池模板: 东兰县| 彭山县| 桃江县| 县级市| 莒南县| 交口县| 珠海市| 邯郸市| 红河县| 邯郸市| 布尔津县| 綦江县| 精河县| 黎城县| 常德市| 崇信县| 肇州县| 舞阳县| 长沙县| 兴业县| 宁强县| 福海县| 盐亭县| 钟山县| 塔河县| 乌什县| 永胜县| 茂名市| 郑州市| 广饶县| 鸡泽县| 富民县| 鄄城县| 科技| 九寨沟县| 洛隆县| 临沭县| 西乌| 赫章县| 咸丰县| 龙里县|