位元詩人 [Rust] 程式設計教學:物件導向 (Object-Oriented Programming)

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

物件導向程式設計 (OOP, object-oriented programming) 是一種程式設計的模式 (paradigm)。由於物件導向是近代軟體開發的主流方法,許多程式語言從語法機制可直接支援,即使像是 C 這種視為非物件導向的語言,也可以用其他語法機制達到類似的效果。 本章會先介紹一般性的物件導向的概念,再介紹如何以 Rust 實作。

物件導向概論

很多語言都直接在語法機制中支援物件導向,然而,每個語言支援的物件導向特性略有不同,像 C++ 的物件系統相當完整,而 Perl 的原生物件系統則相對原始。物件導向在理論上是和語言無關的程式設計模式,但在實務上卻受到語言特性的影響。學習物件導向時,除了學習在某個特定語言下的實作方式外,更應該學習其抽象層次的思維,有時候,暫時放下實作細節,從更高的視角看物件及物件間訊息的流動,對於學習物件導向有相當的幫助。

物件導向是一種將程式碼以更高的層次組織起來的方法。大部分的物件導向以類別 (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):不同子類別共享相同的公開介面,不同語言有不同的繼承機制

以物件導向實作程式,需要從宏觀的角度來思考,不僅要設計單一物件的公開行為,還有物件間如何互動,以達到良好且易於維護的程式碼結構。除了閱讀本書或其他程式設計的書籍以學習如何實作物件外,可閱讀關於 物件導向分析及設計 (object-oriented analysis and design) 或是設計模式 (design pattern) 的書籍,以增進對物件導向的了解。

類別

在 Rust,以 struct 或 enum 定義可實體化的類別,同時,也定義出一個新的型別。見下例:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. struct Point {
  2.     x: f64,
  3.     y: f64
  4. }
  5.  
  6. fn main() {
  7.     let p = Point{ x: 3.0, y: 4.0 };
  8.  
  9.     println!("({}, {})", p.x, p.y);
  10. }

然而,只有屬性而沒有方法的類別不太實用,下文會介紹方法。另外,對於不可實體化的抽象類別,使用 trait 來達成;trait 在物件導向及泛型中都相當重要,我們會於後續相關章節中展示其使用方式。

方法

公開方法和私有方法

方法是類別或物件可執行的動作,公開方法 (public method) 可由物件外存取,而私有方法 (private method) 則僅能由物件內存取。以下為實例:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. // We use mod to create a non-public block
  2. mod lib {
  3.     pub struct Car;
  4.  
  5.     impl Car {
  6.         // Public method
  7.         pub fn run(& self) {
  8.             // Call private method
  9.             self.drive();
  10.         }
  11.     }
  12.  
  13.     impl Car {
  14.         // Private method
  15.         fn drive(& self) {
  16.             println!("Driving a car...");
  17.         }
  18.     }
  19. }
  20.  
  21. fn main() {
  22.     let car = lib::Car{};
  23.     car.run();
  24. }

在本程式中,我們用 mod 建立一個非公開的程式區塊,在其中的程式碼,除非用 pub 明確指定該部分的存取為公開的,否則,一律視為私有的,這是 Rust 在安全性上的設計。

實作方法時,用 impl 區塊包住要實作的方法,在 impl 中,若該方法和某個物件相關,第一個參數要加上 & self 或是 &mut self,視可變性而定。對沒有物件導向經驗的讀者來說,self 是一個令人混淆的概念;基本上,self 是一個特殊的變數,代稱物件,將方法加上 self 這個參數,代表該方法和某個物件相關。以本程式來說,self.drive() 代表這個物件呼叫了 drive 方法。對應到主程式中,self 代換為 car 這個實際的物件實體。簡單的原則在於類別是物件的設計圖,而非物件。

在本程式中,對主程式來說,drive 方法是不存在的,如果把 car.run() 改為 car.drive() 則會引發以下錯誤:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. error: method `drive` is private

由此可知,主程式的確無法存取 drive 方法。

Getters 和 Setters

在先前 Point 的實作,我們直接存取物件的資料,在物件導向中,這不是好的方法。比較好的方法,是將屬性私有化,而用公開方法存取。我們現在建立 Point 類別,這個類別代表 2D 上的點。首先,定義此類別及其屬性:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. pub struct Point {
  2.     x: f64,
  3.     y: f64
  4. }

實作建構子 (constructor) 的部分,建構子是一個特別的函式,用來初始化物件:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. impl Point {
  2.     // Constructor, which is just a regular method
  3.     pub fn new(x: f64, y: f64) -> Point {
  4.         let mut p = Point{ x: 0.0, y: 0.0 };
  5.  
  6.         // Set the fields of Point through setters
  7.         p.set_x(x);
  8.         p.set_y(y);
  9.  
  10.         p
  11.     }
  12. }

new 用來實體化 Point 類別。在物件導向的術語中,稱此特殊方法為建構子;在 Rust,new 只是普通的方法,將 new 改為別的名稱也行,讀者可以將以上程式的 new 改為 create 或別的名稱看看,程式仍能正常運作。不過,一般還是會把實體化物件的方法稱為 new,這是約定俗成的用法。

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. impl Point {
  2.     // Setter for x, private
  3.     fn set_x(&mut self, x: f64) {
  4.         self.x = x;
  5.     }
  6. }
  7.  
  8. impl Point {
  9.     // Setter for y, private
  10.     fn set_y(&mut self, y: f64) {
  11.         self.y = y;
  12.     }
  13. }

在我們的建構子中,我們沒有直接將資料指派到物件屬性,而另外用 setter 來指派;雖然在本例中,我們的 setter 沒有特別的行為,但日後我們需要對資料進行篩選或轉換時,只要修改 setter 方法即可,其他部分的程式碼則不受影響。在撰寫物件導向程式時,我們會儘量將修改的幅度降到最小。

接著,實作 getters:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. impl Point {
  2.     // Getter for x, public
  3.     pub fn x(& self) -> f64 {
  4.         self.x
  5.     }
  6. }
  7.  
  8. impl Point {
  9.     // Getter for y, public
  10.     pub fn y(& self) -> f64 {
  11.         self.y
  12.     }
  13. }

最後,從外部程式使用此類別:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. fn main() {
  2.     let p = Point::new(3.0, 4.0);
  3.  
  4.     println!("({}, {})", p.x(), p.y());
  5. }

由於本程式將物件屬性和 setter 皆設為私有,對外部程式來說,物件建立後不可修改。在撰寫物件時,除了必要的方法外,儘量不要公開物件的其他部分,將公開行為變為私有的,可能會導致程式無法順利執行,應盡量避免。

帶有方法的 enum

使用 enum 也可以建立具有行為的類別。在以下實例中,我們建立 color model 類別。首先,宣告 Color 類別:

在本例中,我們宣告了 RGB (Red-Green-Blue)、CMYK (Cyan-Magenta-Yellow-Black)、HSL (Hue-Saturation-Lightness) 三種 color model,若有需要,也可以宣告其他的 color model。同一個 Color 物件,在同一時間內只會儲存其中一種 color model,不同 color model 間有公式可以轉換,使用 enum 會比使用數個獨立的 struct 來得適合。

實作 RGB model 的建構子,由於 RGB model 是三個 8 位元非負整數來表示顏色的值,故用 u8 來儲存:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. impl Color {
  2.     // Constructor for RGB model
  3.     pub fn from_rgb(r: u8, g: u8, b: u8) -> Color {
  4.         Color::RGB{ r: r, g: g, b: b }
  5.     }
  6. }

實作 CMYK model 的建構子,由於 CMYK 的值為小於等於一百的百分比,我們額外建立一個私有類別方法來檢查值的正確性:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. impl Color {
  2.     // Constructor for CMYK model
  3.     pub fn from_cmyk(c: f64, m: f64, y: f64, k: f64) -> Color {
  4.         if !Self::is_valid_ratio(c) {
  5.             panic!("Invalid Cyan value {} in CMYK model", c);
  6.         }
  7.  
  8.         if !Self::is_valid_ratio(m) {
  9.             panic!("Invalid Magenta value {} in CMYK model", m);
  10.         }
  11.  
  12.         if !Self::is_valid_ratio(y) {
  13.             panic!("Invalid Yellow value {} in CMYK model", y);
  14.         }
  15.  
  16.         if !Self::is_valid_ratio(k) {
  17.             panic!("Invalid Black value {} in CMYK model", k);
  18.         }
  19.  
  20.         Color::CMYK{ c: c, m: m, y: y, k: k }
  21.     }
  22. }
  23.  
  24. impl Color {
  25.     // Private class method used to validate ratio value
  26.     fn is_valid_ratio(n: f64) -> bool {
  27.         0.0 <= n && n <= 1.0
  28.     }
  29. }

實作 HSL model 的建構子,在這裡,我們用整數儲存角度,其值為 [0, 360) 的區間。:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. impl Color {
  2.     // Constructor for HSL model
  3.     pub fn from_hsl(h: u16, s: f64, l: f64) -> Color {
  4.         if !Self::is_valid_hue(h) {
  5.             panic!("Invalid Hue value {} in HSL model", h);
  6.         }
  7.  
  8.         if !Self::is_valid_ratio(s) {
  9.             panic!("Invalid Saturation value {} in HSL model", s);
  10.         }
  11.  
  12.         if !Self::is_valid_ratio(l) {
  13.             panic!("Invalid Lightness value {} in HSL model", l);
  14.         }
  15.  
  16.         Color::HSL{ h: h, s: s, l: l }
  17.     }
  18. }
  19.  
  20. impl Color {
  21.     fn is_valid_hue(n: u16) -> bool {
  22.         n < 360
  23.     }
  24. }

實作 RGB color model 的 getter,若物件內部儲存的 color model 是不同類型,則依公式來轉換:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. impl Color {
  2.     pub fn rgb(& self) -> (u8, u8, u8) {
  3.         match *self {
  4.             Color::RGB{r, g, b} => (r, g, b),
  5.             Color::CMYK{c, m, y, k} => {
  6.                 ((255.0 * (1.0 - c) * (1.0 - k)) as u8,
  7.                  (255.0 * (1.0 - m) * (1.0 - k)) as u8,
  8.                  (255.0 * (1.0 - y) * (1.0 - k)) as u8)
  9.             }
  10.             Color::HSL{h, s, l} => {
  11.                 // Saturation == 0
  12.                 if f64::abs(s) < 1e-10 {
  13.                     return ((255.0 * l) as u8,
  14.                             (255.0 * l) as u8,
  15.                             (255.0 * l) as u8);
  16.                 }
  17.  
  18.                 let v2: f64;
  19.                 let v1: f64;
  20.  
  21.                 if l < 0.5 {
  22.                     v2 = l * (1.0 + s);
  23.                 } else {
  24.                     v2 = (l + s) - s * l
  25.                 }
  26.  
  27.                 v1 = 2.0 * l - v2;
  28.  
  29.                 ((255.0 * Self::hue2rgb(
  30.                     v1, v2, ((h as f64)/360.0 + 1.0/3.0))) as u8,
  31.                  (255.0 * Self::hue2rgb(
  32.                     v1, v2, (h as f64)/360.0)) as u8,
  33.                  (255.0 * Self::hue2rgb(
  34.                     v1, v2, ((h as f64)/360.0 - 1.0/3.0))) as u8)
  35.             }
  36.         }
  37.     }
  38. }
  39.  
  40. impl Color {
  41.     fn hue2rgb(v1: f64, v2: f64, h: f64) -> f64 {
  42.         let mut _h = h;
  43.         if _h < 0.0 {
  44.             _h += 1.0;
  45.         }
  46.  
  47.         if _h > 1.0 {
  48.             _h -= 1.0;
  49.         }
  50.  
  51.         if 6.0 * _h < 1.0 {
  52.             return v1 + (v2 - v1) * 6.0 * _h;
  53.         }
  54.  
  55.         if 2.0 * _h < 1.0 {
  56.             return v2;
  57.         }
  58.  
  59.         if 3.0 * _h < 2.0 {
  60.             return v1 + (v2 - v1) * ((2.0 / 3.0 - _h) * 6.0);
  61.         }
  62.  
  63.         v1
  64.     }
  65. }

最後,從外部程式呼叫:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. fn main() {
  2.     let green = Color::from_hsl(120, 1.0, 0.5);
  3.     let (r, g, b) = green.rgb();
  4.     println!("{} {} {}", r, g, b);  // 0 255 0
  5. }

在本例中,由 HSL 轉至 RGB 的公式較複雜,這裡不詳細講解,有興趣的讀者可自行查詢電腦圖學相關資料。

解構子

若要實作 Rust 類別的解構子 (destructor),實作 Drop trait 即可。由於 Rust 會自動管理資源,純 Rust 實作的類別通常不需要實作解構子,但有時仍需要實作 Drop trait,像是用到 C 語言函式配置記憶體,則需明確於解構子中釋放記憶體。本書將於後續章節展示 Drop trait 的實作。

多型

Rust 使用 trait 來實作多型;透過 trait,Rust 程式可像動態語言般使用不同類別。我們用一個實際的範例來說明。

首先,用 trait 設立共有的公開方法:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. // Use Drive trait as a common type
  2. pub trait Drive {
  3.     fn drive(&self);
  4. }

接著,建立三個不同的汽車類別,這三個類別各自實作 Drive trait:

最後,透過多型的機制由外部程式呼叫這三個類別:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. fn main() {
  2.     let mut cars = Vec::new() as Vec<Box<Drive>>;
  3.     cars.push(Box::new(Toyota));
  4.     cars.push(Box::new(Honda));
  5.     cars.push(Box::new(Ford));
  6.     for c in &cars {
  7.         c.drive();
  8.     }
  9. }

在本例中,ToyotaHondaFord 三個類別實質上是各自獨立的,透過 Drive trait,達成多型的效果,從外部程式的角度來說,這三個物件視為同一個型別,擁有相同的行為。由於 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 的味道。

組合就是將某個類別內嵌在另一個類別中,變成另一個類別的屬性,然後再透過多型提供相同的公開介面,外部程式會覺得好像類別有繼承一般。

假設我們要設計一個 RPG 遊戲,有兩個類別,分別是 Creature 和 Character,而這兩者都可設定行動優先順序。首先,決定公開界面,為了簡化問題,這裡僅實作優先權的 getter/setter:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. pub trait Priority {
  2.     fn get_priority(&self) -> i32;
  3.     fn set_priority(&mut self, value: i32);
  4. }

接著,實作 Creature 類別:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. pub struct Creature {
  2.     priority: i32,
  3.     // Other fields omitted.
  4. }
  5.  
  6. impl Creature {
  7.     pub fn new() -> Creature {
  8.         Creature{ priority: 0 }
  9.     }
  10. }
  11.  
  12. impl Priority for Creature {
  13.      fn get_priority(&self) -> i32 {
  14.          self.priority
  15.      }
  16.  
  17.      fn set_priority(&mut self, value: i32) {
  18.          self.priority = value;
  19.      }
  20. }

接著,實作 Character 類別,在這裡,我們透過組合的機制將 Creature 類別變成 Character 類別的一部分:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. pub struct Character {
  2.     // Creature become a member of Character
  3.    creature: Creature,
  4.  
  5.    // Other field omitted.
  6. }
  7.  
  8. impl Character {
  9.     pub fn new() -> Character {
  10.        let c = Creature::new();
  11.        Character{ creature: c }
  12.     }
  13. }
  14.  
  15. impl Priority for Character {
  16.     fn get_priority(&self) -> i32 {
  17.         self.creature.get_priority()
  18.     }
  19.  
  20.     fn set_priority(&mut self, value: i32) {
  21.         self.creature.set_priority(value);
  22.     }
  23. }

`1 最後,從外部程式呼叫這兩個類別:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. fn main() {
  2.     let mut goblin = Creature::new();
  3.     let mut fighter = Character::new();
  4.  
  5.     println!("The priority of the goblin is {}", goblin.get_priority());
  6.     println!("The priority of the fighter is {}", fighter.get_priority());
  7.  
  8.     println!("Set the priority of the fighter");
  9.     fighter.set_priority(2);
  10.     println!("The priority of the fighter is {} now",
  11.         fighter.get_priority());
  12. }

在本例中,Creature 是 Character 的屬性,但從外部程式看來,無法區分兩者是透過繼承還是組合得到相同的行為。在 Rust 和 Go 這兩個新興語言中,不約而同拿掉繼承的機制,改用組合搭配多型達到類似的效果,這樣的設計,是語言的進步還是語言的缺陷,就留給各位讀者自行思考。

前文提到 struct 無法繼承,而 trait 可以繼承。以下展示一個繼承 trait 的實例。首先,定義 Color 和 Sharp trait,接著由 Item trait 繼承前兩個 trait:

實作 Orange 類別,該類別實作 Color trait:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. pub struct Orange {}
  2.  
  3. impl Color for Orange {
  4.      fn color<'a>(&self) -> &'a str {
  5.          "orange"
  6.     }
  7. }

實作 Ball 類別,該類別實作 Shape trait:

實作 Basketball 類別,該類別透過多型機制置入 Color 和 Shape 兩種類別:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. pub struct Basketball {
  2.      color: Box<Color>,
  3.      shape: Box<Shape>,
  4. }
  5.  
  6. impl Basketball {
  7.     pub fn new() -> Basketball {
  8.         let orange = Box::new(Orange{});
  9.         let ball = Box::new(Ball{});
  10.         Basketball{ color: orange, shape: ball }
  11.     }
  12. }
  13.  
  14. impl Color for Basketball {
  15.     fn color<'a>(&self) -> &'a str {
  16.         self.color.color()
  17.     }
  18. }
  19.  
  20. impl Shape for Basketball {
  21.     fn shape<'a>(&self) -> &'a str {
  22.         self.shape.shape()
  23.     }
  24. }
  25.  
  26. impl Item for Basketball {
  27.     fn name<'a>(&self) -> &'a str {
  28.         "basketball"
  29.     }
  30. }

最後,透過外部程式呼叫 Basketball 類別:

在本程式中,Item 繼承了 Color 和 Shape 的方法,再加上自身的方法,共有三個方法。在實作 Basketball 物件時,則需要同時實作三者的方法;在我們的實作中, color 和 shape 這兩個屬性實際上是物件,由這些內部物件提供相關方法,但由外部程式看起來像是靜態的屬性,這裡利用到物件導向的封裝及組合,但外部程式不需擔心內部的實作,展現物件導向的優點。

在撰寫物件導向程式時,可將 trait 視為沒有內建狀態的抽象類別。然而,trait 類別本身不能實體化,要使用 Box 等容器來實體化。這些容器使用到泛型的觀念,在本章,我們暫時不會深入泛型的概念,基本上,透過泛型,同一套程式碼可套用在不同型別上。

方法重載

方法重載指的是相同名稱的方法,但參數不同。Rust 不支援方法重載,如果有需要的話,可以用泛型結合抽象類別來模擬,可見泛型章節中的範例。

運算子重載

Rust 的運算子重載利用了內建 trait 來完成。每個 Rust 的運算子,都有相對應的 trait,在實作新類別時,只要實作某個運算子對應的 trait,就可以直接使用該運算子。運算子重載並非物件導向必備的功能,像是 Java 和 Go 就沒有提供運算子重載的機制。善用運算子重載,可減少使用者記憶公開方法的負擔,但若運算子重載遭到濫用,則會產生令人困惑的程式碼。

在本範例中,我們實作有理數 (rational number) 及其運算,為了簡化範例,本例僅實作加法。首先,宣告有理數型別,一併呼叫要實作的 trait:

Data hosted with ♥ by Pastebin.com - Download Raw - See Original
  1. // Trait for binary '+' operator
  2. use std::ops::Add;
  3.  
  4. // Trait for formatted string
  5. use std::fmt;
  6.  
  7. // Automatically implement Copy and Clone trait
  8. // Therefore, our class acts as a primitive type
  9. #[derive(Copy, Clone)]
  10. pub struct Rational {
  11.     num: i32,
  12.     denom: i32,
  13. }

實作其建構子,在內部,我們會求其最大公約數後將其約分:

實作加法運算,在這裡,實作 Add trait,之後就可以直接用 + 運算子來計算:

我們額外實作 Display trait,方便我們之後可以直接從終端機印出有理數:

最後,從外部程式呼叫此有理數物件。由於我們實作了相關的 trait,使用起來很類似基礎型別:

如果我們繼續將這個範例發展下去,就可以實作有理數的運算系統,不過,這只是展示運算子重載的範例,我們就把實作有理數的任務留給有興趣的讀者。在本例中,我們實作了兩個 trait,Rust 自動幫我們實作兩個 trait,使得外部程式使用起來相當接近操作內建形別的過程,體現運算子重載的優點。

關於作者

位元詩人 (ByteBard) 是資訊領域碩士,喜歡用開源技術來解決各式各樣的問題。這類技術跨平台、重用性高、技術生命長。

除了開源技術以外,位元詩人喜歡日本料理和黑咖啡,會一些日文,有時會自助旅行。