今天的主題是建立一個 Class Person
(不過我們待會就會看到不是每個語言都有所謂的 Class),而 Person
class 有一個屬性是 age
, 當建立 Instance 時,Constructor 會吃一個參數叫做 initialAge
,並把 initialAge
設給 age
。如果 initialAge
是負的,就會把 age
設成 0
。這個 Class 有兩個方法,第一個是 yearPasses()
,會把 age 增加 1;第一個方法是 amIOld()
,會根據年齡印出不同的值。那我們就開始吧!
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)
。這也讓 getPerson
和 apply
是相同的效果囉,所以我們會傾向寫成 apply
的這種形式。以後我們有機會會介紹。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.
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()
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。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.
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()
}
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)yearPasses
不是接收 *person
而是 person
,那麼在 invoke 這個 Function 的時候,會是拷貝一份 person
到這個 Function 裡頭來使用,那麼既然是拷貝的,就算加了一百歲,也不過是這個拷貝上去增加,本來的 person
是不會改變的。但如果是 *person
,那就會傳 reference 進來這個 Function,而改到的也會跟外面的是同一份,就達到我們想要修改同一個 instance 的目的啦!interface
,裡頭定義不含實作的方法,而假使我們定義了一個 struct 並且實作了這個 interface 所有的方法,那麼我們這個 struct 就可以在任何需要這個 interface 作為參數的 Function 被當作參數傳入然後被使用囉!也就是說,假使我們的 struct 實作了 interface,就可以跟別人的東西接在一起囉!棒棒!17
You are a teenager.
18
You are old.
#![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。Person
的 instance,也就是 new
這個 Function。在看 new
之前,我們先看到 impl Person
這個區塊,這邊的意思就是我們要來實作 Person
的方法啦!new
, amIOld
跟 yearPasses
三個 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 基本上的參數是 &self
, self
就是 instance 本身 (跟 Python 很像),至於一定要有 &
就是之前所解釋的,跟所有權有關囉!(可參考 day 0)。而其中 yearPasses
因為會改變到 instance 本身,所以要傳入的參數必須明確指定是可被修改的,所以就會是 &mut self
。而這兩個 Function 因為第一個參數都是 self
,所以可以直接透過 instance 用 . 來 invoke 囉!impl
的區塊唷!所以我們可以把不同作用的 Function 包在不同的 impl
區塊,相當自由。而 impl
也會用來讓 Struct 去實作 Trait,簡單來說,一個 Trait 裡頭就定義了很多函式特徵 (尚未實作的函式),通常會把一些 shared behavior 包在一個 Trait 讓很多 Struct 可以去實作。如果現在有興趣的話可以參考這裡囉!You are a teenager.
You are old.
今天的內容很重要也很充實,也可以看出這些語言比較明顯不同的概念了,希望大家看了會很有感覺。明天見囉!