面向对象

物件导向是一种将程式码以更高的层次组织起来的方法。大部分的物件导向以类别(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 定义可实体化的类别。

  1. struct Point {
  2. x : f64 ,
  3. y : f64
  4. }
  5. fn main ( ) {
  6. let p = Point { x : 3.0 , y : 4.0 } ;
  7. println ! ( "({}, {})" , p.x , p.y ) ;
  8. }

只有属性而没有方法的类别不太实用,另外,对于不可实体化的抽象类别,使用trait 来达成;trait 在物件导向及泛型中都相当重要

方法

公开方法和私有方法:

方法是类别或物件可执行的动作,公开方法(public method) 可由物件外存取,而私有方法(private method) 则仅能由物件内存取。以下为实例:

  1. mod lib {
  2. pub struct Car ;
  3. impl Car {
  4. // Public method
  5. pub fn run ( & self ) {
  6. // Call private method
  7. self .drive ( ) ;
  8. }
  9. // Private method
  10. fn drive ( & self ) {
  11. println ! ( "Driving a car..." ) ;
  12. }
  13. }
  14. }
  15. fn main ( ) {
  16. let car = lib :: Car { } ;
  17. car.run ( ) ;
  18. }

Getters 和Setters

我们的setter 没有特别的行为,但日后我们需要对资料进行筛选或转换时,只要修改setter 方法即可,其他部分的程式码则不受影响。在撰写物件导向程式时,我们会尽量将修改的幅度降到最小。

  1. pub struct Point {
  2. x : f64 ,
  3. y : f64,
  4. }
  5. impl Point {
  6. // Constructor, which is just a regular method
  7. pub fn new ( x : f64 , y : f64 ) -> Point {
  8. let mut p = Point { x : 0.0 , y : 0.0 } ;
  9. // Set the fields of Point through setters
  10. p.set_x ( x ) ;
  11. p.set_y ( y ) ;
  12. p
  13. }
  14. // Setter for x, private
  15. fn set_x ( & mut self , x : f64 ) {
  16. self .x = x ;
  17. }
  18. // Setter for y, private
  19. fn set_y ( & mut self , y : f64 ) {
  20. self .y = y ;
  21. }
  22. // Getter for x, public
  23. pub fn x ( & self ) -> f64 {
  24. self .x
  25. }
  26. // Getter for y, public
  27. pub fn y ( & self ) -> f64 {
  28. self .y
  29. }
  30. }
  31. fn main ( ) {
  32. let p = Point :: new ( 3.0 , 4.0 ) ;
  33. println ! ( "({}, {})" , p.x ( ) , p.y ( ) ) ;
  34. }

解构

若要实作Rust 类别的解构子(destructor),实作Drop trait 即可。由于Rust 会自动管理资源,纯Rust 实作的类别通常不需要实作解构子,但有时仍需要实作Drop trait,像是用到C 语言函式配置记忆体,则需明确于解构子中释放记忆体。

多态

Rust使用trait来实作多态;透过trait,Rust程式可像动态语言般使用不同类别。我们用一个实际的范例来说明。

首先,用trait设立共有的公开方法:

  1. pub trait Drive {
  2. fn drive ( & self ) ;
  3. }
  4. // 建立三个不同的汽车类别,这三个类别各自实作Drivetrait:
  5. pub struct Toyota ;
  6. impl Drive for Toyota {
  7. fn drive ( & self ) {
  8. println ! ( "Driving a Toyota car" ) ;
  9. }
  10. }
  11. pub struct Honda ;
  12. impl Drive for Honda {
  13. fn drive ( & self ) {
  14. println ! ( "Driving a Honda car" ) ;
  15. }
  16. }
  17. pub struct Ford ;
  18. impl Drive for Ford {
  19. fn drive ( & self ) {
  20. println ! ( "Driving a Ford car" ) ;
  21. }
  22. }
  23. // 透过多型的机制由外部程式呼叫这三个类别:
  24. fn main ( ) {
  25. let mut cars = Vec :: new ( ) as Vec < Box < Drive >>;
  26. cars.push ( Box :: new ( Toyota ) ) ;
  27. cars.push ( Box :: new ( Honda ) ) ;
  28. cars.push ( Box :: new ( Ford ) ) ;
  29. for c in & cars {
  30. c.drive ( ) ;
  31. }
  32. }

在本例中,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 的味道。

组合就是将某个类别内嵌在另一个类别中,变成另一个类别的属性,然后再透过多型提供相同的公开介面,外部程式会觉得好像类别有继承一般。

  1. pub trait Priority {
  2. fn get_priority ( & self ) -> i32 ;
  3. fn set_priority ( & mut self , value : i32 ) ;
  4. }
  5. pub struct Creature {
  6. priority : i32 ,
  7. // Other fields omitted.
  8. }
  9. impl Creature {
  10. pub fn new ( ) -> Creature {
  11. Creature { priority : 0 }
  12. }
  13. }
  14. impl Priority for Creature {
  15. fn get_priority ( & self ) -> i32 {
  16. self .priority
  17. }
  18. fn set_priority ( & mut self , value : i32 ) {
  19. self .priority = value ;
  20. }
  21. }
  22. // 我们透过组合的机制将Creature 类别变成Character 类别的一部分:
  23. pub struct Character {
  24. // Creature become a member of Character
  25. creature : Creature ,
  26. // Other field omitted.
  27. }
  28. impl Character {
  29. pub fn new ( ) -> Character {
  30. let c = Creature :: new ( ) ;
  31. Character { creature : c }
  32. }
  33. }
  34. impl Priority for Character {
  35. fn get_priority ( & self ) -> i32 {
  36. self .creature.get_priority ( )
  37. }
  38. fn set_priority ( & mut self , value : i32 ) {
  39. self .creature.set_priority ( value ) ;
  40. }
  41. }
  42. fn main ( ) {
  43. let mut goblin = Creature :: new ( ) ;
  44. let mut fighter = Character :: new ( ) ;
  45. println ! ( "The priority of the goblin is {}" , goblin.get_priority ( ) ) ;
  46. println ! ( "The priority of the fighter is {}" , fighter.get_priority ( ) ) ;
  47. println ! ( "Set the priority of the fighter" ) ;
  48. fighter.set_priority ( 2 ) ;
  49. println ! ( "The priority of the fighter is {} now" ,
  50. fighter.get_priority ( ) ) ;
  51. }

Creature 是Character 的属性,但从外部程式看来,无法区分两者是透过继承还是组合得到相同的行为。struct 无法继承,而trait 可以继承。

  1. // 。首先,定义Color 和Sharp trait,接着由Item trait 继承前两个trait:
  2. pub trait Color {
  3. fn color < 'a>(&self) -> &' a str ;
  4. }
  5. // Parent trait
  6. pub trait Shape {
  7. fn shape < 'a>(&self) -> &' a str ;
  8. }
  9. // Child trait
  10. pub trait Item : Color + Shape {
  11. fn name < 'a>(&self) -> &' a str ;
  12. }
  13. // 实作Orange 类别,该类别实作Color trait:
  14. pub struct Orange { }
  15. impl Color for Orange {
  16. fn color < 'a>(&self) -> &' a str {
  17. "orange"
  18. }
  19. }
  20. // 实作Ball 类别,该类别实作Shape trait:
  21. pub struct Ball { }
  22. impl Shape for Ball {
  23. fn shape < 'a>(&self) -> &' a str {
  24. "ball"
  25. }
  26. }
  27. // 实作Basketball 类别,该类别透过多型机制置入Color 和Shape 两种类别:
  28. pub struct Basketball {
  29. color : Box < Color >,
  30. shape : Box < Shape >,
  31. }
  32. impl Basketball {
  33. pub fn new ( ) -> Basketball {
  34. let orange = Box :: new ( Orange { } ) ;
  35. let ball = Box :: new ( Ball { } ) ;
  36. Basketball { color : orange , shape : ball }
  37. }
  38. }
  39. impl Color for Basketball {
  40. fn color < 'a>(&self) -> &' a str {
  41. self .color.color ( )
  42. }
  43. }
  44. impl Shape for Basketball {
  45. fn shape < 'a>(&self) -> &' a str {
  46. self .shape.shape ( )
  47. }
  48. }
  49. impl Item for Basketball {
  50. fn name < 'a>(&self) -> &' a str {
  51. "basketball"
  52. }
  53. }
  54. fn main ( ) {
  55. let b : Box < Item >= Box :: new ( Basketball :: new ( ) ) ;
  56. println ! ( "The color of the item is {}" , b.color ( ) ) ;
  57. println ! ( "The shape of the item is {}" , b.shape ( ) ) ;
  58. println ! ( "The name of the item is {}" , b.name ( ) ) ;
  59. }

在本程式中,Item 继承了Color 和Shape 的方法,再加上自身的方法,共有三个方法。在实作Basketball 物件时,则需要同时实作三者的方法;在我们的实作中, color 和shape 这两个属性实际上是物件,由这些内部物件提供相关方法,但由外部程式看起来像是静态的属性,这里利用到物件导向的封装及组合,但外部程式不需担心内部的实作,展现物件导向的优点。

在撰写物件导向程式时,可将trait 视为没有内建状态的抽象类别。然而,trait 类别本身不能实体化,要使用Box等容器来实体化。这些容器使用到泛型的观念,在本章,我们暂时不会深入泛型的概念,基本上,透过泛型,同一套程式码可套用在不同型别上。

方法重载

方法重载指的是相同名称的方法,但参数不同。Rust 不支援方法重载,如果有需要的话,可以用泛型结合抽象类别来模拟,可见泛型范例。

运算子重载

Rust 的运算子重载利用了内建trait 来完成。每个Rust 的运算子,都有相对应的trait,在实作新类别时,只要实作某个运算子对应的trait,就可以直接使用该运算子。运算子重载并非物件导向必备的功能,像是Java 和Go 就没有提供运算子重载的机制。善用运算子重载,可减少使用者记忆公开方法的负担,但若运算子重载遭到滥用,则会产生令人困惑的程式码。

在本范例中,我们实作有理数(rational number) 及其运算,为了简化范例,本例仅实作加法。首先,宣告有理数型别,一并呼叫要实作的trait:

  1. use std :: ops :: Add ;
  2. // Trait for formatted string
  3. use std :: fmt ;
  4. // Automatically implement Copy and Clone trait
  5. // Therefore, our class acts as a primitive type
  6. # [ derive ( Copy , Clone ) ]
  7. pub struct Rational {
  8. num : i32 ,
  9. denom : i32 ,
  10. }
  11. // 实作其建构子,在内部,我们会求其最大公约数后将其约分:
  12. impl Rational {
  13. pub fn new ( p : i32 , q : i32 ) -> Rational {
  14. if q == 0 {
  15. panic ! ( "Denominator should not be zero." ) ;
  16. }
  17. let d = Rational :: gcd ( p , q ) .abs ( ) ;
  18. Rational { num : p / d , denom : q / d }
  19. }
  20. }
  21. impl Rational {
  22. fn gcd ( a : i32 , b : i32 ) -> i32 {
  23. if b == 0 {
  24. a
  25. } else {
  26. Rational :: gcd ( b , a % b )
  27. }
  28. }
  29. }
  30. // 实作加法运算,在这里,实作Add trait,之后就可以直接用+ 运算子来计算:
  31. impl Add for Rational {
  32. type Output = Rational ;
  33. fn add ( self , other : Rational ) -> Rational {
  34. let p = self .nu​​m * other.denom + other.num * self .denom ;
  35. let q = self .denom * other.denom ;
  36. Rational :: new ( p , q )
  37. }
  38. }
  39. // 我们额外实作Display trait,方便​​我们之后可以直接从终端机印出有理数:
  40. impl fmt :: Display for Rational {
  41. fn fmt ( & self , f : & mut fmt :: Formatter ) -> fmt :: Result {
  42. let is_positive = ( self .nu​​m > 0 && self .denom > 0 )
  43. || ( self .nu​​m < 0 && self .denom < 0 ) ;
  44. let sign = if is_positive { "" } else { "-" } ;
  45. write ! ( f , "{}{}/{}" , sign , self .nu​​m.abs ( ) , self .denom.abs ( ) )
  46. }
  47. }
  48. fn main ( ) {
  49. let a = Rational :: new ( - 1 , 4 ) ;
  50. let b = Rational :: new ( 1 , 5 ) ;
  51. // Call operator-overloaded binary '+' operator
  52. let y = a + b ;
  53. // Call operator-overloaded formated string
  54. println ! ( "{} + {} = {}" , a , b , y ) ;
  55. }