面向对象
物件导向是一种将程式码以更高的层次组织起来的方法。大部分的物件导向以类别(class) 为基础,透过类别可产生实际的物件(object) 或实体(instance) ,类别和物件就像是饼干模子和饼干的关系,透过同一个模子可以产生很多片饼干。物件拥有属性(field) 和方法(method),属性是其内在状态,而方法是其外在行为。透过物件,状态和方法是连动的,比起传统的程序式程式设计,更容易组织程式码。
许多物件导向语言支援封装(encapsulation),透过封装,程式设计者可以决定物件的那些部分要对外公开,那些部分仅由内部使用,封装不仅限于静态的资料,决定物件应该对外公开的行为也是封装。当多个物件间互动时,封装可使得程式码容易维护,反之,过度暴露物件的内在属性和细部行为会使得程式码相互纠结,难以除错。
物件间可以透过组合(composition)再利用程式码。物件的属性不一定要是基本型别,也可以是其他物件。组合是透过有… (has-a)关系建立物件间的关连。例如,汽车物件有引擎物件,而引擎物件本身又有许多的状态和行为。继承(inheritance)是另一个再利用程式码的方式,透过继承,子类别(child class)可以再利用父类别(parent class)的状态和行为。继承是透过是… (is-a)关系建立物件间的关连。例如,研究生物件是学生物件的特例。然而,过度滥用继承,容易使程式码间高度相依,造成程式难以维护。可参考组合胜过继承(composition over inheritance)这个指导原则来设计自己的专案。
透过多型(polymorphism) 使用物件,不需要在意物件的实作,只需依照其公开介面使用即可。例如,我们想用汽车物件执行开车这项行为,不论使用Honda 汽车物件或是Ford 汽车物件,都可以执行开车这项行为,而不需在意不同汽车物件间的实作差异。多型有许多种形式,如:
- 特定多态(ad hoc polymorphism):
- 函数重载(functional overloading):同名而不同参数型别的方法(method)
- 运算子重载(operator overloading) : 对不同型别的物件使用相同运算子(operator)
- 泛型(generics):对不同型别使用相同实作
- 子类型(Subtyping):不同子类别共享相同的公开介面,不同语言有不同的继承机制
以物件导向实作程式,需要从宏观的角度来思考,不仅要设计单一物件的公开行为,还有物件间如何互动,以达到良好且易于维护的程式码结构。
类别
在Rust,以struct 或enum 定义可实体化的类别。
struct Point {x : f64 ,y : f64}fn main ( ) {let p = Point { x : 3.0 , y : 4.0 } ;println ! ( "({}, {})" , p.x , p.y ) ;}
只有属性而没有方法的类别不太实用,另外,对于不可实体化的抽象类别,使用trait 来达成;trait 在物件导向及泛型中都相当重要
方法
公开方法和私有方法:
方法是类别或物件可执行的动作,公开方法(public method) 可由物件外存取,而私有方法(private method) 则仅能由物件内存取。以下为实例:
mod lib {pub struct Car ;impl Car {// Public methodpub fn run ( & self ) {// Call private methodself .drive ( ) ;}// Private methodfn drive ( & self ) {println ! ( "Driving a car..." ) ;}}}fn main ( ) {let car = lib :: Car { } ;car.run ( ) ;}
Getters 和Setters
我们的setter 没有特别的行为,但日后我们需要对资料进行筛选或转换时,只要修改setter 方法即可,其他部分的程式码则不受影响。在撰写物件导向程式时,我们会尽量将修改的幅度降到最小。
pub struct Point {x : f64 ,y : f64,}impl Point {// Constructor, which is just a regular methodpub fn new ( x : f64 , y : f64 ) -> Point {let mut p = Point { x : 0.0 , y : 0.0 } ;// Set the fields of Point through settersp.set_x ( x ) ;p.set_y ( y ) ;p}// Setter for x, privatefn set_x ( & mut self , x : f64 ) {self .x = x ;}// Setter for y, privatefn set_y ( & mut self , y : f64 ) {self .y = y ;}// Getter for x, publicpub fn x ( & self ) -> f64 {self .x}// Getter for y, publicpub fn y ( & self ) -> f64 {self .y}}fn main ( ) {let p = Point :: new ( 3.0 , 4.0 ) ;println ! ( "({}, {})" , p.x ( ) , p.y ( ) ) ;}
解构
若要实作Rust 类别的解构子(destructor),实作Drop trait 即可。由于Rust 会自动管理资源,纯Rust 实作的类别通常不需要实作解构子,但有时仍需要实作Drop trait,像是用到C 语言函式配置记忆体,则需明确于解构子中释放记忆体。
多态
Rust使用trait来实作多态;透过trait,Rust程式可像动态语言般使用不同类别。我们用一个实际的范例来说明。
首先,用trait设立共有的公开方法:
pub trait Drive {fn drive ( & self ) ;}// 建立三个不同的汽车类别,这三个类别各自实作Drivetrait:pub struct Toyota ;impl Drive for Toyota {fn drive ( & self ) {println ! ( "Driving a Toyota car" ) ;}}pub struct Honda ;impl Drive for Honda {fn drive ( & self ) {println ! ( "Driving a Honda car" ) ;}}pub struct Ford ;impl Drive for Ford {fn drive ( & self ) {println ! ( "Driving a Ford car" ) ;}}// 透过多型的机制由外部程式呼叫这三个类别:fn main ( ) {let mut cars = Vec :: new ( ) as Vec < Box < Drive >>;cars.push ( Box :: new ( Toyota ) ) ;cars.push ( Box :: new ( Honda ) ) ;cars.push ( Box :: new ( Ford ) ) ;for c in & cars {c.drive ( ) ;}}
在本例中,Toyota、Honda和Ford三个类别实质上是各自独立的,透过Drivetrait,达成多型的效果,从外部程式的角度来说,这三个物件视为同一个型别,拥有相同的行为。由于trait无法直接实体化,必需借助Box<T>等容器才能将其实体化,Box<T>会从堆积(heap)配置记忆体,并且不需要解参考,相当于C/C++的smart pointer。
组合胜于继承
Rust 的struct 和enum 无法继承,而trait 可以继承,而且trait 支援多重继承(multiple inheritance) 的机制;trait 可提供介面和实作,但本身无法实体化,反之,struct 和enum 可以实体化。Rust 用这样的机制避开C++ 的菱型继承(diamond inheritance) 问题,类似Java 的interface 的味道。
组合就是将某个类别内嵌在另一个类别中,变成另一个类别的属性,然后再透过多型提供相同的公开介面,外部程式会觉得好像类别有继承一般。
pub trait Priority {fn get_priority ( & self ) -> i32 ;fn set_priority ( & mut self , value : i32 ) ;}pub struct Creature {priority : i32 ,// Other fields omitted.}impl Creature {pub fn new ( ) -> Creature {Creature { priority : 0 }}}impl Priority for Creature {fn get_priority ( & self ) -> i32 {self .priority}fn set_priority ( & mut self , value : i32 ) {self .priority = value ;}}// 我们透过组合的机制将Creature 类别变成Character 类别的一部分:pub struct Character {// Creature become a member of Charactercreature : Creature ,// Other field omitted.}impl Character {pub fn new ( ) -> Character {let c = Creature :: new ( ) ;Character { creature : c }}}impl Priority for Character {fn get_priority ( & self ) -> i32 {self .creature.get_priority ( )}fn set_priority ( & mut self , value : i32 ) {self .creature.set_priority ( value ) ;}}fn main ( ) {let mut goblin = Creature :: new ( ) ;let mut fighter = Character :: new ( ) ;println ! ( "The priority of the goblin is {}" , goblin.get_priority ( ) ) ;println ! ( "The priority of the fighter is {}" , fighter.get_priority ( ) ) ;println ! ( "Set the priority of the fighter" ) ;fighter.set_priority ( 2 ) ;println ! ( "The priority of the fighter is {} now" ,fighter.get_priority ( ) ) ;}
Creature 是Character 的属性,但从外部程式看来,无法区分两者是透过继承还是组合得到相同的行为。struct 无法继承,而trait 可以继承。
// 。首先,定义Color 和Sharp trait,接着由Item trait 继承前两个trait:pub trait Color {fn color < 'a>(&self) -> &' a str ;}// Parent traitpub trait Shape {fn shape < 'a>(&self) -> &' a str ;}// Child traitpub trait Item : Color + Shape {fn name < 'a>(&self) -> &' a str ;}// 实作Orange 类别,该类别实作Color trait:pub struct Orange { }impl Color for Orange {fn color < 'a>(&self) -> &' a str {"orange"}}// 实作Ball 类别,该类别实作Shape trait:pub struct Ball { }impl Shape for Ball {fn shape < 'a>(&self) -> &' a str {"ball"}}// 实作Basketball 类别,该类别透过多型机制置入Color 和Shape 两种类别:pub struct Basketball {color : Box < Color >,shape : Box < Shape >,}impl Basketball {pub fn new ( ) -> Basketball {let orange = Box :: new ( Orange { } ) ;let ball = Box :: new ( Ball { } ) ;Basketball { color : orange , shape : ball }}}impl Color for Basketball {fn color < 'a>(&self) -> &' a str {self .color.color ( )}}impl Shape for Basketball {fn shape < 'a>(&self) -> &' a str {self .shape.shape ( )}}impl Item for Basketball {fn name < 'a>(&self) -> &' a str {"basketball"}}fn main ( ) {let b : Box < Item >= Box :: new ( Basketball :: new ( ) ) ;println ! ( "The color of the item is {}" , b.color ( ) ) ;println ! ( "The shape of the item is {}" , b.shape ( ) ) ;println ! ( "The name of the item is {}" , b.name ( ) ) ;}
在本程式中,Item 继承了Color 和Shape 的方法,再加上自身的方法,共有三个方法。在实作Basketball 物件时,则需要同时实作三者的方法;在我们的实作中, color 和shape 这两个属性实际上是物件,由这些内部物件提供相关方法,但由外部程式看起来像是静态的属性,这里利用到物件导向的封装及组合,但外部程式不需担心内部的实作,展现物件导向的优点。
在撰写物件导向程式时,可将trait 视为没有内建状态的抽象类别。然而,trait 类别本身不能实体化,要使用Box等容器来实体化。这些容器使用到泛型的观念,在本章,我们暂时不会深入泛型的概念,基本上,透过泛型,同一套程式码可套用在不同型别上。
方法重载
方法重载指的是相同名称的方法,但参数不同。Rust 不支援方法重载,如果有需要的话,可以用泛型结合抽象类别来模拟,可见泛型范例。
运算子重载
Rust 的运算子重载利用了内建trait 来完成。每个Rust 的运算子,都有相对应的trait,在实作新类别时,只要实作某个运算子对应的trait,就可以直接使用该运算子。运算子重载并非物件导向必备的功能,像是Java 和Go 就没有提供运算子重载的机制。善用运算子重载,可减少使用者记忆公开方法的负担,但若运算子重载遭到滥用,则会产生令人困惑的程式码。
在本范例中,我们实作有理数(rational number) 及其运算,为了简化范例,本例仅实作加法。首先,宣告有理数型别,一并呼叫要实作的trait:
use std :: ops :: Add ;// Trait for formatted stringuse std :: fmt ;// Automatically implement Copy and Clone trait// Therefore, our class acts as a primitive type# [ derive ( Copy , Clone ) ]pub struct Rational {num : i32 ,denom : i32 ,}// 实作其建构子,在内部,我们会求其最大公约数后将其约分:impl Rational {pub fn new ( p : i32 , q : i32 ) -> Rational {if q == 0 {panic ! ( "Denominator should not be zero." ) ;}let d = Rational :: gcd ( p , q ) .abs ( ) ;Rational { num : p / d , denom : q / d }}}impl Rational {fn gcd ( a : i32 , b : i32 ) -> i32 {if b == 0 {a} else {Rational :: gcd ( b , a % b )}}}// 实作加法运算,在这里,实作Add trait,之后就可以直接用+ 运算子来计算:impl Add for Rational {type Output = Rational ;fn add ( self , other : Rational ) -> Rational {let p = self .num * other.denom + other.num * self .denom ;let q = self .denom * other.denom ;Rational :: new ( p , q )}}// 我们额外实作Display trait,方便我们之后可以直接从终端机印出有理数:impl fmt :: Display for Rational {fn fmt ( & self , f : & mut fmt :: Formatter ) -> fmt :: Result {let is_positive = ( self .num > 0 && self .denom > 0 )|| ( self .num < 0 && self .denom < 0 ) ;let sign = if is_positive { "" } else { "-" } ;write ! ( f , "{}{}/{}" , sign , self .num.abs ( ) , self .denom.abs ( ) )}}fn main ( ) {let a = Rational :: new ( - 1 , 4 ) ;let b = Rational :: new ( 1 , 5 ) ;// Call operator-overloaded binary '+' operatorlet y = a + b ;// Call operator-overloaded formated stringprintln ! ( "{} + {} = {}" , a , b , y ) ;}
