iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 5
0
Software Development

30 天把自己榨好榨滿的四週四語言大挑戰!系列 第 5

[Day 4] 類別與結構你選誰?

今天的主題是建立一個 Class Person(不過我們待會就會看到不是每個語言都有所謂的 Class),而 Person class 有一個屬性是 age, 當建立 Instance 時,Constructor 會吃一個參數叫做 initialAge,並把 initialAge 設給 age。如果 initialAge 是負的,就會把 age 設成 0。這個 Class 有兩個方法,第一個是 yearPasses(),會把 age 增加 1;第一個方法是 amIOld(),會根據年齡印出不同的值。那我們就開始吧!


Scala

package com.ryan.scala.oop

object Person {
    def getPerson(initialAge: Int) =
        if (initialAge < 0) new Person(0) else new Person(initialAge)

    def apply(initialAge: Int) =
        if (initialAge < 0) new Person(0) else new Person(initialAge)
}

class Person(val age: Int) {
    import Person._

    def amIOld(): Unit = {
        age match {
            case a if 0 to 12 contains a => println("You are young.")
            case a if 13 to 17 contains a => println("You are a teenager.")
            case _ => println("You are old.")
        }
    }

    def yearPasses(): Person = getPerson(age + 1)
}

object Solution {
    def main(args: Array[String]) {
        val p0 = Person.getPerson(17)
        val p1 = Person(17)
        p1.amIOld()
        val p2 = p1.yearPasses()
        p2.amIOld()
    }
}
  • 首先我們可以看到我們宣告了一個 class Person(val age: Int)age 是 constructor parameter,而 age 前面有 val 表示 age 是可以在外部被存取的,例如 a = new Person(1); print(a.age) 會印出 1,但如果 age 前面沒有 val 則反之無法外部存取,但要注意的是,我們是用 val表示 age是不可以被修改的。先不管目前裡頭宣告的兩個 method,其實當我們 initiate 一個新的 instance 時,Class 大括號內的所有內容都會被執行,可以視為是一個 constructor。當然我們也可以自定義甚至 overload constructor,實際上是宣告一個 this 的 method,但在 Scala 我不建議這樣用,我們將會使用 Factory Pattern。
  • 現在來講為什麼 Class 的上面,又有一個 Object Person 。這個 Object Person 叫做 Class Person 的 companion object,我們可以在這 Object裡頭定義變數和方法,效果就好像是 Class 的 static variables 和 static methods。而我們可以在這個 Object 定義 Factory method,來幫助我們做一些處理後產生新的 instance,就如同這裡的 getPerson Method,我們根據給的參數做一些判斷來生出不同的 instance。那 apply 又是什麼呢?這是 Scala 提供的語法糖,當我們要使用 apply 這個 method 時,可以不用寫出 apply ,也就是本來如果我們要 Person.apply(..) ,可以直接寫成 Person(..) ,所以可以看到當我們在 main 裡面宣告 p1 時,是直接寫 val p1 = Person(17) 。這也讓 getPersonapply 是相同的效果囉,所以我們會傾向寫成 apply 的這種形式。以後我們有機會會介紹。
  • Scala 還有一個東西叫做 case class,就是在宣告 Class 的時候,多加上了 case 這個關鍵字,會幫我們自動生成其 Companion object,以及 apply method 囉!(可參考這個這個)
  • 最後來看看 yearPasses() 這個函數,正常來說,這個函數執行之後應該是會把 instance 的 attribute age 加 1,但是我們這裡的 age 是 immutable,所以在 Scala 這種 Functional paradigm 的語言,我們盡量都讓所有東西是 immutable,好處是可以避免 Side effect (每次執行的結果都是可以預期的)。所以這裡的 yearPasses() 是直接產生一個新的 instance,然後 age 是本來的加 1 囉!
  • 以上的程式結果如下。和你想得一樣嗎?
You are a teenager.
You are old.

Python 3

class Person:
    def __init__(self, initialAge):
        if initialAge < 0:
          self.age = 0
        else:
          self.age = initialAge
          
    def amIOld(self): 
        if 0 <= self.age < 13:
            print('You are young.')
        elif 13 <= self.age < 18:
            print('You are a teenager.')
        else:
            print('You are old.')
            
    def yearPasses(self): 
        self.age += 1
        
p1 = Person(17)
p1.amIOld()
p1.yearPasses()
p1.amIOld()
  • 首先我們先來看 Python 的 constructor,也就是 def __init__(self, initialAge) 。這裡表示當我們在 initialize Person 的 instance 時,必須要給入一個參數也就是 initialAge ,而我們可以在裡頭去對我們的參數做一切處理並對初始化一個 instance。 self 指的就是這個 instance 本身,我們直接用 self.age 就表示給這個 instance 一個屬性叫 age 。之後我們如果要 access age ,就用 <instance_name>.age 就可以了。
  • amIOld 這個 method 就不特別講,基本上就是根據 age 印出不同內容。而 yearPasses 我們每次執行就會把 age 的值再加 1。
  • 接著我們來看為什麼每一個 method 都一定要有第一個參數 self 呢?這其實是個 Python 的語法糖所致。當我們例如用 p1.yearPasses() 時,真正會執行的是 Person.yearPasses(p1) ,這也是為什麼 Class 內的 Method 都一定要有 self 當第一個參數啦!那麼為什麼 __init__ 也需要 self 參數呢?我們不是還沒做 initialize,為什麼就有 instance 可以當第一個參數?(雞生蛋生雞),那是因為當一個 instance 被創建的時候其實是先 call __new__ 這個 method,而這個 method 才是真正地把 instance 給建立起來,然後才 call __init__ 去幫我們初始化。至於是誰 call __new__ 的呢?嘿嘿,那就是每個 Class 的爸爸 (Class 的 Class) Object 的 __call__ Method 啦!至於為什麼 __call__ 會被 call 到呢?留給大家去研究研究囉!(提示: <class_name>(<arguments>) 這樣寫其實是會做什麼?)
  • 以上會印出的結果是:
You are a teenager.
You are old.

Golang

package main 
import "fmt"

type person struct {
	age int
}

func NewPerson(initialAge int) *person {
  var age int
  if initialAge < 0 {
    age = 0
  } else {
    age = initialAge
  }
  return &person{age: age}
}

func (p *person) amIOld() {
    fmt.Println(p.age)
    if p.age > 18 {
        fmt.Println("You are old.")
    } else if p.age < 18 && p.age >= 13 {
        fmt.Println("You are a teenager.")
    } else {
        fmt.Println("You are young.")
    }
}

func (p *person) yearPasses() {
    p.age += 1
}

func main() {
  p1 := NewPerson(17)
  p1.amIOld()
  p1.yearPasses()
  p1.amIOld()
}
  • 在 Golang,並沒有所謂的 Class,而是 Struct。Struct 並不像 Class 有屬性、有方法,而是只有多個有名字的欄位,就像是我們這裡定義, type Person struct 有一個欄位叫做 age 而且型別是 int 。而 Golang 的 Struct 也沒有所謂的 constructor,而我們能用的方式就是定義一個 Function,來作為 Factory function。就像這裡的 func NewPerson(initialAge int) *person ,我們在 Function 裡面處理一些邏輯後去產生一個 instance,如同這 Function 的最後一行 return &person{age: age}person{age: age} 是表示我們要產生一個 Struct person 的 instance,而且屬性 age 等於 var age 。最後我們看到有個 &person 前面,表示我們不是回傳 instance 本身,而是回傳 instance 的 pointer,也就是這個 instance 的位址。
  • 接著我們看到 func (p *person) yearPasses() ,這個 Function 就是讓我們可以把一個 person instance 的 age 加 1。但我們看到 func 的後面不是直接接 Function 名,而是有 (p *person) ,這是為什麼呢?這部分在 Golang 叫做 receiver,如果像我們這邊是 *person 表示這是個 function with pointer receiver,但如果今天是 (p person) 就是 function with value receiver。
  • yearPasses 這時就是一個會接收 *person 的 Function。既然都接收了,當然可以在 Function 裡頭用囉!(Function 當然也還是可以自己定義參數),就像這個 Function 裡頭的 p.age += 1 。至於要怎麼 call 這個 Function,就如同 main裡頭的 p1.yearPasses() ,這時候我們就可以直接用 instance 或是 instance 的 pointer 來 invoke yearPasses() 啦。什麼?這裡不是定義 pointer receiver 嗎? 怎麼直接用 instance 也行?對!沒錯,也可以唷!這是 Golang 開的方便之門。但是如果今天不是 receiver,而是參數的話,那就不能把 pointer of instance 和 instance 混用囉 (例如 func xxx(p *person) 那麼參數就一定要給 pointer)
  • 最後我們來講 value receiver 和 pointer receiver 的差別,其實就是類似 call by value 和 call by reference 的差別。例如說如果我們的 yearPasses 不是接收 *person 而是 person ,那麼在 invoke 這個 Function 的時候,會是拷貝一份 person 到這個 Function 裡頭來使用,那麼既然是拷貝的,就算加了一百歲,也不過是這個拷貝上去增加,本來的 person 是不會改變的。但如果是 *person ,那就會傳 reference 進來這個 Function,而改到的也會跟外面的是同一份,就達到我們想要修改同一個 instance 的目的啦!
  • 順道一提就是除了 struct ,Golang 有另一個可以定義的 Type 叫 interface ,裡頭定義不含實作的方法,而假使我們定義了一個 struct 並且實作了這個 interface 所有的方法,那麼我們這個 struct 就可以在任何需要這個 interface 作為參數的 Function 被當作參數傳入然後被使用囉!也就是說,假使我們的 struct 實作了 interface,就可以跟別人的東西接在一起囉!棒棒!
  • 最後附上程式執行完的結果。
17
You are a teenager.
18
You are old.

Rust

#![allow(non_snake_case)]

struct Person {
    age: i32
}

impl Person {
    fn new(initialAge: i32) -> Person {
        if initialAge < 0 {
            println!("Age is not valid, setting age to 0.");
            return Person { age: 0 };
        }
        return Person { age: initialAge };
        
    }
 
    fn amIOld(&self) {
        if self.age < 13 { println!("You are young.") } 
        else if self.age >= 13 && self.age < 18 { println!("You are a teenager.") }
        else { println!("You are old.") }
    }
 
    fn yearPasses(&mut self) {
        self.age += 1;
    }
}

fn main() {
    let mut p = Person::new(17);
    p.amIOld();
    p.yearPasses();
    p.amIOld();
}
  • 首先看到第一行 #![allow(non_snake_case)] 這是因為 Rust 建議變數和函式的命名要用 snake case (也就是用底線區隔的命名法),但是因為 Hackerrank 自帶部分的 code 是用 non-snake case,所以加上 #![allow(non_snake_case)] 可以跟 Compiler 說不用去檢查命名的部分唷!不然會有 warning。
  • Rust 和 Golang 類似,也是一樣是 Struct,而沒有 class。所以跟 Golang 一樣,我們也是定義了一個 Method 來幫助我們進行一些邏輯處理後來生出 struct Person 的 instance,也就是 new 這個 Function。在看 new 之前,我們先看到 impl Person 這個區塊,這邊的意思就是我們要來實作 Person 的方法啦!
  • 我們來看這裡面分別有 new , amIOldyearPasses 三個 Function,其中只有 new 的參數沒有 self ,這樣的 Function 我們稱做 associated function,也就是 Person 這個 Struct 本身關聯的函數,就不會先產生 instance 再來 invoke,就是有點 static 的味道。而使用的方式就像是 main 裡頭的 let mut p = Person::new(age) 啦!而 new 的功能就是 return 給我們一個 instance of Person( Person {age: initialAge})。另外兩個有 self 的 Function,先來看兩個 Function 基本上的參數是 &selfself 就是 instance 本身 (跟 Python 很像),至於一定要有 & 就是之前所解釋的,跟所有權有關囉!(可參考 day 0)。而其中 yearPasses 因為會改變到 instance 本身,所以要傳入的參數必須明確指定是可被修改的,所以就會是 &mut self 。而這兩個 Function 因為第一個參數都是 self,所以可以直接透過 instance 用 . 來 invoke 囉!
    有趣的是, 同一個 Struct 可以有很多 impl 的區塊唷!所以我們可以把不同作用的 Function 包在不同的 impl 區塊,相當自由。而 impl 也會用來讓 Struct 去實作 Trait,簡單來說,一個 Trait 裡頭就定義了很多函式特徵 (尚未實作的函式),通常會把一些 shared behavior 包在一個 Trait 讓很多 Struct 可以去實作。如果現在有興趣的話可以參考這裡囉!
  • 以上程式會印出
You are a teenager.
You are old.

結語

今天的內容很重要也很充實,也可以看出這些語言比較明顯不同的概念了,希望大家看了會很有感覺。明天見囉!


上一篇
[Day 3] 我的程式不失控!
下一篇
[Day 5] 又回到最初的起點! (迴圈剖析)
系列文
30 天把自己榨好榨滿的四週四語言大挑戰!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言