17784 views
# [物件導向 Ep. 1] 類別與物件 ###### tags: `MCL` `oop` Declaration ~ 此系列內容強調物件導向的部分,因此不會參雜太多語法介紹。會建議使用 C++ 或是 Java 當作基底語言去學習,C++ 的原因是他有完整的物件導向特性;Java 也有 (甚至有時候還比 C++ 更好理解些),只是他的開發環境比較囉嗦;Python 本身是全物件導向,但是他有部分類別操作是比較複雜的,所以只會帶一些簡單的例子輔佐。 ## 前言 物件導向程式設計 (Object Oriented Programming, OOP) 是一種概念,他傳達著說當我們在操作一團變數與方法的時候,可以朝向著 - 把**相關的變數與方法們封裝成一個「東西」**,讓使用者**透過這個東西去操作這些內容物** (可能是變數或是函數方法)。 - 當兩種不同的類型有強烈的歸屬關係,如「水」跟「熱水」,時,「熱水」**可以透過某種方式繼承「水」那邊的內容物** (比如測量公升數方法之類的),**而不用自己重寫**。 - 且當**多種不種類型有共同的歸屬關係**,如「貓」跟「狗」都是「動物」,「動物」都有叫聲,時,可以**透過某種途徑對每個東西呼叫一樣的函數,但是呈現出不一樣的行為**。一個經典的例子是:貓的叫聲是「喵」,而狗的叫聲是「汪」,但他們都是動物的叫聲。 這些行為可以幫助我們增強程式碼的結構性,並且減少了程式碼的重複性。而實現上面三種行為的基本元素,就是類別 (Class) 與物件 (Object)。 ### 類別與物件的關係 ![](https://hackmd.mcl.math.ncu.edu.tw/uploads/upload_fbb39fdb4e4641952c8fa95647f869ed.png) > [name=[What is Object Oriented Programming (OOP)](https://javatutorial.net/java-oop)] 上圖是一個有趣的例子,他很簡要地說明了類別跟物件的關係。一言蔽之, - 類別 (Class) 是**架構定義**, - 物件 (Object) 是**根據架構宣告出來的變數**。 類別與物件的好處在於可以把其相關的方法包裝在物件 (變數) 當中,而不用特別在外部設計函數,避免還要花功夫理解物件內部到底是怎麼運作的。 ### 案例 舉例來說,在 C++ 當中, ```cpp= std::string foo("Alice"); std::cout << foo.length() << std::endl; ``` 其中你可以觀察到: - `std::string` 是一種類別 (型別),而 `foo` 即是生成出來的物件 (變數),並且在建立的時候把 `"Alice"` 字串作為其內部值。 - `length()` 是包裝在 `foo` 物件裡面的一個方法 (函數),他會負責回傳該字串的長度。 - 你不會看到 `length()` 方法在內部的處理方式,他已經包裝起來了 (封裝 Encapsulation)。 > 對於 C++ 的字串,你可能比較常看到以下 > ```cpp > std::string foo = "Alice"; > ``` > 的用法,原因是 C++ 會幫你實現複製初始化 (Copy initialization) 的行為,從正統物件初始的寫法,他會是下面的形式: > ```cpp > std::string foo("Alice"); > ``` > - 假若使用 `explicit` 關鍵字,copy initialization 就會失效。 另外一個例子,來看 Python 3: ```python= foo = str('Alice') print(foo.startswith('A')) # string `foo` starts with 'A' ``` 同樣地概念, - `<class 'str'>` 是一種類別 (型態)。惟因為 Python 採動態型別 (宣告變數的時候不需要指派型態),所以你需要用 `type(foo)` 去查看他的當前型態。 - 而 `foo` 是生成出來的物件 (變數),並且在建立的時候把 `"Alice"` 字串作為其內部值。 - `startswith()` 是包裝在 `foo` 物件裡面的一個方法 (函數),他會檢查該字串是不是由某個字串當起頭。 - 你不會看到 `startswith` 方法內部的處理方式,他已經包裝起來了 (封裝 Encapsulation)。 ### 支援物件導向的語言 在文章開頭處有提到,物件導向本身是一種概念,所以你有機會在不同的語言看到相似的用法,不同之處可能只在於語法,或者是內部一些細微的差異。 現在大多數的語言都支援物件導向程式設計模式: - C++ - Java - Python 2/3 - JavaScript (ES6^) - PHP - Ruby - Kotlin - etc. 不過也有一些語言是不支援這種模式的,如: - C - Golang (Go Language) > 他們也有自己的包裝方法:結構 (Structure),但他是弱化的類別,沒有物件導向的特性。 > ```c= > // c structure > struct student { > char name[10]; > int age; > }; > typedef struct student student; > ``` > ```go= > // golang structure > type Student struct { > name string > age int > }; > ``` ## 類別的用法 從上面的鋪陳中,你依稀會發現一個類別應該涵蓋一些項目: * 裡面有一些變數和一些方法,我們會把他們稱為成員變數跟成員方法。 * 更細節的來說,你可能會希望有些成員不能讓物件外直接取用 (比如一個字串儲存貓叫聲的變數)。 * 在物件生成的時候,你可以把傳進來的值做些處理,比如把值 `"Alice"` 儲存到內部的某個變數當中。我們會用一個函數包裝裡面的動作,這個函數有一個特殊的名詞叫建構子 (Constructor)。 了解這些需求後,我們直接看範例: - C++ ```cpp= #include <iostream> using namespace std; class Cat { public: Cat(string animalName) { name = animalName; barking = "meow"; } void saySomething() { cout << name << ": " << barking << endl; } private: string name; string barking; }; int main() { Cat c1("Alice"); Cat c2("Bob"); c1.saySomething(); c2.saySomething(); return 0; } ``` :::spoiler Java ```java= // App.java class Cat { private String name; private String barking; public Cat(String animalName) { name = animalName; barking = "meow"; } public void saySomething() { System.out.printf("%s: %s\n", name, barking); } } public class App { public static void main(String[] args) throws Exception { Cat c1 = new Cat("Alice"); Cat c2 = new Cat("Bob"); c1.saySomething(); c2.saySomething(); return 0; } } ``` ::: :::spoiler Python 3 ```python= class Cat(Object): def __init__(self, animalName): self.name = animalName self.barking = 'meow' def saySomething(self): print('{}: {}'.format(self.name, self.barking)) if __name__ == '__main__': c1 = Cat('Alice') c2 = Cat('Bob') c1.saySomething() c2.saySomething() ``` * Python 沒有私人變數或是私人方法,他的變相作法是在變數或函數前面加上一個底線 `_`. * 但他終究做不到上面的 `private` 功效,所以在這邊的例子就不特別加了。 ::: :::spoiler JavaScript ES6^ ```javascript= class Cat { constructor(animalName) { this.name = animalName; this.barking = "meow"; } saySomething() { console.log(`${this.name}: ${this.barking}`); } } c1 = new Cat("Alice"); c2 = new Cat("Bob"); c1.saySomething(); c2.saySomething(); ``` ::: 從上面的例子中,你會發現 * 類別名稱是 `Cat`,裡面有兩個變數 `name` `barking` 跟一個方法 `saySomething()` * 他們都有一個建構子,建構子的參數叫做 `animalName`,過程中他會把他的值指派給成員變數 `name` ### 物件關鍵字 如果參數名字跟成員變數名字衝突的時候,你可以使用關鍵字去強調你要調用的是成員變數: - C++: `this` ```cpp= #include <iostream> using namespace std; class Cat { public: Cat(string name) { this->name = name; this->barking = "meow"; } void saySomething() { cout << this->name << ": " << this->barking << endl; } private: string name; string barking; }; int main() { Cat c1("Alice"); Cat c2("Bob"); c1.saySomething(); c2.saySomething(); return 0; } ``` > * `this` 在 C++ 代表的是指標,調用物件指標之成員的方式是用 `->` :::spoiler Java: `this` ```java= // App.java class Cat { private String name; private String barking; public Cat(String name) { this.name = name; this.barking = "meow"; } public void saySomething() { System.out.printf("%s: %s\n", this.name, this.barking); } } public class App { public static void main(String[] args) throws Exception { Cat c1 = new Cat("Alice"); Cat c2 = new Cat("Bob"); c1.saySomething(); c2.saySomething(); return 0; } } ``` ::: :::spoiler Python 3: `self` ```python= class Cat(Object): def __init__(self, animalName): self.name = animalName self.barking = 'meow' def saySomething(self): print('{}: {}'.format(self.name, self.barking)) if __name__ == '__main__': c1 = Cat('Alice') c2 = Cat('Bob') c1.saySomething() c2.saySomething() ``` ::: :::spoiler JavaScript ES6^: `this` ```javascript= class Cat { constructor(animalName) { this.name = animalName; this.barking = "meow"; } saySomething() { console.log(`${this.name}: ${this.barking}`); } } c1 = new Cat("Alice"); c2 = new Cat("Bob"); c1.saySomething(); c2.saySomething(); ``` ::: ### 公開成員與私有成員 公開與私有不同之處在於:公開成員可以從外部取用,但私有不行。 從上述例子 (C++) 當中,`saySomething()` 是公開成員,所以你可以使用 ```cpp Cat c1("Alice"); c1.saySomething(); ``` 去使用他的功能。 但 `name` 跟 `barking` 是私有成員,所以當你做以下任何動作時都會發生錯誤: ```cpp Cat c1("Alice"); cout << c1.name; // will raise compile error cout << c1.barking; // also will raise comiple error ``` > 這樣的特性並不局限於你的成員是變數還是函數,重點是他是 `public` 還是 `private`. ### 慣用寫法 習慣來說,我們會把變數跟部份的函數放到私有,而想要讓人使用的函數放到公開去。 但一般程式還是有可能需要存取變數值的需求,但你的選擇不會是把變數放到公開去,這意味著該值可以被使用者任意修改。相對地,針對一個變數,我們會傾向另外開兩個函數空該分別進行取值 (Getter) 跟賦值 (Setter) 行為。 以 `name` 為例,我們可以這麼做: ```cpp= class Cat { public: // ... string getName() { return this->name; } void setName(string name) { if (name.length() > 10) { cerr << "name.length() should <= 10" << endl; } this->name = name; } private: string name; }; ``` 接著你就可以透過存取 `Cat` 物件中的 `name` 成員變數: ```cpp Cat c1("Alice"); cout << c1.getName() << endl; // Alice c1.setName("Bob"); cout << c1.getName() << endl; // Bob ``` 你會發現當你呼叫 `setName()` 的時候,他會去檢查你取的名字長度是否超過 10 個字元,如果違反就會輸出錯誤訊息。這便是透過函數控制的好處:你可以加入自己定義的檢查機制,避免你所操作的值會發生不預期的狀況。