National Yunlin University of Science and Technology
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try a Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

1

Local AI

--

Uedu Code

--

雲林科技大學 AQI 56 27°C PM2.5 16
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 14

物件導向程式設計

類別與物件、建構子、可見度、繼承、介面、trait 與 static
Interaction實作 35 minute(s)

Learning Objectives

  • 定義類別、用 new 建立物件,說明屬性、方法與 $this 的關係,並用建構子(含屬性提升)設定初始值
  • 用 public、protected、private 控制可見度,透過方法封裝資料與檢查規則
  • 說明物件是 handle:指派與傳入函式不會複製物件(和陣列不同),需要複本時用 clone
  • 使用 static 屬性與方法、類別常數,分辨 self 與 static
  • 用 extends 繼承與覆寫方法,用 abstract 類別與 interface 規定子類別要做什麼,寫出多型的程式
  • 認識 trait、__toString、readonly 屬性與 enum

把資料和操作它的函式放在一起

到目前為止,資料放在陣列裡,處理資料的是一個個獨立的函式:

$account = ['owner' => 'Amy', 'balance' => 0];
$account = deposit($account, 500);      // 誰都可以直接寫 $account['balance'] = 1000000;

程式變大之後,「哪些函式可以處理這種陣列」「哪些欄位可以直接改」都只能靠大家自己記得。 物件導向(object-oriented programming,OOP)把資料和操作這些資料的函式包在一起,變成一個類別(class):

名詞 意思 例子
類別(class) 藍圖:規定一種東西有哪些資料、能做哪些事 BankAccount
物件(object) 照著藍圖做出來的一個個實體,也叫實例(instance) Amy 的帳戶、Ken 的帳戶
屬性(property) 物件裡的資料 $owner、$balance
方法(method) 類別裡的函式,操作這個物件的資料 deposit()、getBalance()

你其實已經用過物件了:第 10 課的 DateTimeImmutable、第 13 課的例外都是。這一課學怎麼自己寫類別。 現代的 PHP 框架(Laravel、Symfony)與套件幾乎全部以類別組成,看懂物件導向是讀懂它們的前提。

類別、物件與 $this

class Student                        // 類別名稱習慣用大寫開頭(PascalCase)
{
    public string $name;             // 屬性:可以宣告型別,也可以給預設值
    public array $scores = [];

    public function __construct(string $name)   // 建構子:new 的時候自動執行
    {
        $this->name = $name;
    }

    public function addScore(int $score): void  // 方法
    {
        $this->scores[] = $score;
    }
}

$amy = new Student('Amy');           // 建立物件,'Amy' 傳給建構子
$amy->addScore(90);                  // 呼叫方法:物件->方法()
echo $amy->name;                     // 讀取屬性:物件->屬性(屬性名稱前面不加 $)

方法裡的 $this 代表「呼叫這個方法的那個物件」:$amy->addScore(90) 執行時,$this 就是 $amy; $ken->addScore(70) 執行時,$this 就是 $ken。同一段方法的程式,作用在不同的物件上。

建立物件與 $this

兩次 new Student(…) 在右邊做出兩個物件。進入方法時,看框架裡的 $this 箭頭指向哪一個物件:第 26、27 行呼叫 $amy 的方法,$this 指向 Amy;第 28 行換成 Ken。

建構子屬性提升與可見度

建構子常常只是把參數一個個存進屬性。PHP 8.0 起可以在建構子的參數前面加上可見度,PHP 會自動宣告屬性並指派,叫做建構子屬性提升(constructor property promotion):

// 傳統寫法                                     // PHP 8.0 的屬性提升,效果相同
class Point {                                  class Point {
    public int $x;                                 public function __construct(
    public int $y;                                     public int $x = 0,
    public function __construct(int $x = 0,            public int $y = 0,
                                int $y = 0) {      ) {}
        $this->x = $x;                         }
        $this->y = $y;
    }
}

可見度決定誰可以存取屬性與方法:

關鍵字 類別自己 子類別 類別外面 物件圖上的標記
public ✓ ✓ ✓ (無)
protected ✓ ✓ ✗ #
private ✓ ✗ ✗ -

從外面存取 private 屬性會丟出 Error: Cannot access private property …。

封裝(encapsulation)的作法:屬性設成 private,外面只能透過 public 方法讀寫,方法負責檢查規則(存款不能是負數、Email 要合法)。 這樣不論程式有多少地方用到這個類別,資料都不會被改成不合理的值。讀取用的方法叫 getter(getBalance()),修改用的叫 setter(setEmail()); 只有真的需要被修改的屬性才提供 setter。

型別屬性(PHP 7.4):宣告了型別、又沒有預設值的屬性,在指派之前是「未初始化」,讀取它會丟出 Error: Typed property Student::$name must not be accessed before initialization。所以要在建構子裡設定,或給預設值。

readonly 屬性(PHP 8.1):只能在類別內部指派一次,之後誰都不能修改,適合帳號、建立時間這類不該變的資料。 修改時丟出 Error: Cannot modify readonly property …。

static 與類別常數

一般的屬性每個物件各有一份;static 屬性屬於類別本身,全部物件共用一份,用 類別名稱::$屬性 或在類別裡用 self::$屬性 存取。 static 方法不需要建立物件就能呼叫(BankAccount::count()),裡面沒有 $this。 類別常數用 const 宣告,不能修改,用 類別名稱::常數 讀取。

寫法 屬於 類別裡怎麼用 外面怎麼用
private int $balance 每個物件 $this->balance $a->balance(public 時)
private static int $count 類別 self::$count BankAccount::$count(public 時)
public const MIN_DEPOSIT = 100 類別 self::MIN_DEPOSIT BankAccount::MIN_DEPOSIT

在下面的動畫裡,static 屬性畫在「靜態屬性」框。

封裝、readonly 與 static

$balance 是 private,物件圖上前面有 -;$owner 用屬性提升宣告成 public readonly。看「靜態屬性」框裡的 BankAccount::$count:每建立一個物件就加 1,兩個物件共用這一個數字。第 29~31 行三次嘗試違反規則,都被擋下來了。

物件是 handle:指派不會複製

第 08 課說過:陣列是值,$b = $a 會複製一份。物件不一樣:變數裡存的是一個指向物件的 handle(可以想成門牌號碼), $b = $a 只複製了門牌號碼,兩個變數指向同一個物件,透過任何一個修改,另一個都看得到。把物件傳給函式也一樣,函式可以修改到原本的物件。

陣列 物件
$b = $a 複製一份,各改各的 指向同一個物件
傳給函式 函式拿到複本 函式拿到同一個物件
要一份獨立的複本 指派就是複本 $b = clone $a;
== 鍵與值都相同 同一個類別、屬性值都相同
=== 鍵、值、型別、順序都相同 是同一個物件

clone 是淺複製(shallow copy):新物件的屬性是原本屬性的複本,但如果某個屬性本身是物件,複製的只是 handle, 兩個物件會共用那個內層物件。需要連內層一起複製時,在類別裡寫 __clone(),clone 完成後會自動呼叫它:

class Post
{
    public Tag $tag;
    public function __clone()
    {
        $this->tag = clone $this->tag;   // 深複製:內層物件也複製一份
    }
}

物件共用 vs 陣列複製

先看陣列:第 9 行執行完,右邊出現兩個陣列。再看物件:第 15 行執行完,$box1 和 $box2 的箭頭指向同一個 Box。第 20 行的 clone 才真的做出第二個 Box。

想一想:物件、可見度與 clone

1.

下面的程式會印出什麼?

<?php
class Counter { public int $n = 0; }
function bump(Counter $c) { $c->n++; }
function bumpArr(array $a) { $a['n']++; }
$c = new Counter();
$arr = ['n' => 0];
bump($c);
bumpArr($arr);
echo $c->n, ' ', $arr['n'];
2.

下面的程式執行的結果是什麼?

<?php
class User {
    private string $password = 'secret';
}
$u = new User();
echo $u->password;
3.

下面的程式會印出什麼?

<?php
class Tag { public string $name = 'php'; }
class Post {
    public Tag $tag;
    public array $likes = [];
    public function __construct() { $this->tag = new Tag(); }
}
$a = new Post();
$b = clone $a;
$b->tag->name = 'js';
$b->likes[] = 'Amy';
echo $a->tag->name, ' ', count($a->likes);
第 1 次作答

繼承與覆寫

繼承(inheritance):class Dog extends Animal 讓 Dog 擁有 Animal 所有的屬性與方法,再加上自己的東西。 Animal 是父類別(parent class),Dog 是子類別(child class)。PHP 一個類別只能 extends 一個父類別。

  • 覆寫(override):子類別寫一個同名的方法,取代父類別的版本
  • parent::方法():在子類別裡呼叫父類別原本的版本,常用在「先做父類別的事,再多做一點」,以及建構子 parent::__construct(…)
  • 父類別的方法裡呼叫 $this->speak(),執行的是物件實際所屬類別的版本:Dog 物件就執行 Dog 的 speak。這是多型的基礎
  • protected 的屬性子類別可以用,類別外面不行;private 連子類別都不能直接用
  • final:final class 不能被繼承,final public function 不能被覆寫
  • instanceof:$x instanceof Animal 檢查物件是不是這個類別(或它的子類別、它實作的介面)

「什麼時候該用繼承」的判斷:子類別是一種父類別(Dog 是一種 Animal)才用繼承;只是想共用幾個方法,用後面介紹的 trait 或把物件當成屬性(組合)比較合適。

繼承、覆寫與 parent::

三隻寵物都呼叫同一個 intro()(寫在 Animal 裡),但第 9 行的 $this->speak() 會依物件實際的類別,分別執行 Dog、Cat、Animal 的 speak。注意框架名稱:Animal::intro 裡呼叫到 Dog::speak。

abstract 類別與 interface

抽象類別(abstract class):只能被繼承、不能直接 new(會丟出 Error: Cannot instantiate abstract class Shape)。 裡面可以有寫好的方法,也可以有只宣告、不寫內容的抽象方法,子類別一定要實作,否則子類別也只能是抽象的。

介面(interface):只列出「一定要有哪些 public 方法」,不寫任何實作。類別用 implements 承諾會實作這些方法, 一個類別可以實作多個介面(用逗號分隔),這是和 extends 最大的不同。

interface HasArea {
    public function area(): float;
}
abstract class Shape implements HasArea, Stringable {   // 一次實作兩個介面
    abstract public function name(): string;            // 留給子類別
    public function __toString(): string { … }          // 共同的部分寫在這裡
}
abstract class interface
能不能 new 不能 不能
有沒有寫好的方法 可以有 沒有
屬性 可以有 不行(可以有常數)
一個類別可以有幾個 extends 一個 implements 多個

多型(polymorphism):程式只依賴介面(「有 area() 方法的東西」),不管實際是圓形還是長方形; 之後新增三角形,只要它實作 HasArea,計算總面積的程式完全不用改。參數型別也可以寫介面:function total(HasArea ...$shapes)。

__toString() 是魔術方法:物件被當成字串使用(echo $s、字串串接)時自動呼叫。PHP 8.0 起,有 __toString 的類別會自動實作 Stringable 介面。

介面、抽象類別與多型

迴圈裡的 $s 有時是圓、有時是長方形,程式寫的都是同一句 $s->area()。echo $s 會自動呼叫 __toString();它寫在抽象類別 Shape 裡,再呼叫子類別實作的 name() 和 area()。

想一想:繼承與 static

1.

下面的程式會印出什麼?

<?php
class A {
    public function hi() { return 'A'; }
    public function run() { return $this->hi(); }
}
class B extends A {
    public function hi() { return 'B' . parent::hi(); }
}
echo (new B)->run();
2.

下面的程式會印出什麼?

<?php
class Model {
    public static function create(): static { return new static(); }
    public static function who(): string { return self::class . '/' . static::class; }
}
class User extends Model {}
echo User::who(), ' ', get_class(User::create());
第 1 次作答

self 與 static

在類別裡指「類別本身」有兩種寫法,差別只在有繼承的時候:

  • self:: 指寫這段程式碼的那個類別,在寫的時候就決定了
  • static:: 指實際被呼叫的那個類別,執行時才決定(late static binding,延遲靜態綁定)

上一題的 Model::create() 寫成 new static(),子類別 User::create() 就會建立 User;寫成 new self() 則永遠建立 Model。 框架裡「User::find(1) 回傳 User 物件」這類設計就是靠 static。一般情況用 self 即可。

trait 與 enum

trait:一組可以「貼進」類別的屬性與方法。PHP 只能單一繼承,幾個不相關的類別(Post、Comment、User)都需要相同的功能時, 寫成 trait,再在類別裡 use 它,效果就像把程式碼複製進去。一個類別可以 use 多個 trait。

trait HasTimestamps {
    private string $createdAt = '';
    public function touch(string $time): void { $this->createdAt = $time; }
}
class Post {
    use HasTimestamps;        // Post 物件現在有 touch() 方法
}

enum(列舉,PHP 8.1):一個型別只能是幾個固定值之一,例如文章狀態、訂單狀態、撲克牌花色。比起用字串 'draft' 表示, 打錯字('darft')時 PHP 會直接報錯,參數型別也可以寫成 Status $s。

enum Suit {                          // 純 enum(pure enum):只有名字
    case Hearts;
    case Spades;
}

enum Status: string {                // backed enum:每個 case 對應一個字串或整數
    case Draft = 'draft';
    case Published = 'published';
}
寫法 結果
Status::Draft 一個 enum 值;用 === 比較
Status::Draft->name 'Draft'(case 的名字)
Status::Draft->value 'draft'(backed enum 才有)
Status::cases() 所有 case 組成的陣列
Status::from('published') 從值找出 case;找不到丟出 ValueError
Status::tryFrom('deleted') 從值找出 case;找不到回傳 null

enum 也可以有方法,常搭配 match ($this) 為每個 case 回傳不同的結果。資料庫裡存的是 ->value,讀出來再用 from() 變回 enum。

trait 與 enum

Post 透過 use HasTimestamps 得到 touch() 與 createdAt(),框架名稱顯示成 Post::touch。$status 屬性的型別是 enum Status,第 29 行用資料庫常見的字串值 published 換回 enum。

想一想:enum

1.

下面的程式會印出什麼?

<?php
enum Size: int {
    case S = 1;
    case M = 2;
    case L = 3;
}
$s = Size::tryFrom(4) ?? Size::M;
echo $s->name, $s->value, count(Size::cases());
第 1 次作答

動手做:購物車類別

完成 Cart 類別:add() 在數量小於 1 時丟出 InvalidArgumentException('數量至少是 1'),否則把商品記進 private 的 $items;total() 回傳所有商品「單價 × 數量」的總和。下面的主程式不用改。

你的輸出
預期輸出

重點回顧

  • 類別是藍圖、物件是實體:new 類別(…) 建立物件、$物件->方法() 呼叫、$物件->屬性 讀寫;方法裡的 $this 是呼叫它的那個物件
  • __construct 在 new 時執行;PHP 8.0 的屬性提升 public function __construct(private int $x) 一次完成宣告與指派
  • 可見度:public 誰都可以、protected 自己與子類別、private 只有自己;從外面存取 private 丟出 Error。用方法封裝資料與檢查規則
  • 型別屬性在指派前是未初始化,讀取會丟出 Error;readonly(PHP 8.1)只能指派一次
  • 物件是 handle:指派、傳入函式都指向同一個物件(陣列則會複製);clone 做出淺複製,需要深複製時寫 __clone()
  • static 屬性與方法屬於類別,用 類別:: 或 self:: 存取;const 是類別常數;static:: 指實際被呼叫的類別
  • extends 繼承、同名方法覆寫、parent:: 呼叫父類別版本、final 禁止繼承或覆寫、instanceof 檢查類別
  • abstract 類別不能 new,抽象方法由子類別實作;interface 只規定方法,一個類別可以 implements 多個;依賴介面寫程式就是多型
  • trait 用 use 把方法貼進類別;__toString 讓物件可以被 echo
  • enum(PHP 8.1):純 enum 與 backed enum,->name、->value、cases()、from()(找不到丟 ValueError)、tryFrom()(找不到回傳 null)
這一課完成了!
Next lesson:命名空間
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。