← PHP面向对象编程:多态性的核心机制与实战解析 PHP 接口:纯粹的“能力契约” →

PHP 抽象类:为什么不能直接用它创建对象?

原创 2026-05-12 PHP 已有人查阅

在PHP里,有一类很特殊的“半成品”类,它就是抽象类(Abstract Class)。你可以把它理解成一个模具的半成品——它定义了基本形状,但缺少关键的细节,所以不能直接拿来“浇筑”出对象。一旦尝试这样做,PHP 解释器会立刻抛出一个致命错误:PHP Fatal error: Uncaught Error: Cannot instantiate abstract class

这个设计并不是缺陷,而是一种刻意为之的约束。它的核心思想可以用两句话概括:抽象类不能自我实例化,必须被继承。它的价值在于为子类提供统一的结构模板。

定义抽象类的语法很直接,使用 abstract 关键词即可:

abstract class MyClass {
    // 类体
}

任何试图 $obj = new MyClass(); 的操作都是无效的。这就是抽象类最根本的规则。

抽象方法:抽象类的灵魂

一个抽象类里可以包含属性、常量、普通方法,就像任何普通类一样。但它真正区别于普通类的地方,在于可以定义抽象方法(Abstract Method)。抽象方法只有方法签名(名字和参数),没有具体实现,它仅仅是一个需要被履行的“契约”。

这里有一个非常重要的规则:一旦一个类包含了至少一个抽象方法,那么这个类本身也必须被声明为抽象类。 反过来,在普通类里声明抽象方法是语法错误。

看下面这个会出错的例子,它能帮你理解这条规则的强制性:

class myclass {
   abstract function absmethod($arg1, $arg2);
   function method() {
      echo "Hello";
   }
}

执行这段代码,你会看到类似这样的错误信息:PHP Fatal error: Class myclass contains 1 abstract method and must therefore be declared abstract。这很明确地告诉我们,只要类里存在未实现的抽象方法,这个类就必须用 abstract 标记,不允许有“漏网之鱼”。

继承与实现:子类的责任

当一个子类通过 extends 关键字继承一个抽象类时,它实际上与父类签订了一份“合约”:父类中所有抽象方法,子类都必须提供具体的实现。如果子类遗漏了任何一个抽象方法的实现,那么在尝试实例化它时,同样会触发致命错误。

我个人在早期学习时,习惯先写一个什么都不做的空实现,来通过语法检查,然后再逐步填充业务逻辑。这虽然是个取巧的办法,但能避免被频繁的致命错误打断思路。

来看下面这个典型的错误示例:

abstract class BaseClass {
    abstract public function performAction($param1, $param2);
    public function greet() {
        echo "Hello";
    }
}

class DerivedClass extends BaseClass {
    public function showMessage() {
        echo "World";
    }
}

$instance = new DerivedClass();
$instance->greet();

这段代码会抛出错误,因为 DerivedClass 继承了抽象的 performAction() 方法,却没有实现它。正确的做法是,在 DerivedClass 中把 performAction() 补全。修正后的代码应该是这样:

abstract class BaseClass {
    abstract public function performAction($param1, $param2);
    public function greet() {
        echo "Hello";
    }
}

class DerivedClass extends BaseClass {
    public function performAction($param1, $param2) {
        echo "Action performed with $param1 and $param2";
    }
    public function showMessage() {
        echo "World";
    }
}

$instance = new DerivedClass();
$instance->greet(); // 输出: Hello

这里 performAction() 至少有了一个基础的函数体,合约才算履行完毕。它不像接口那样严格要求方法签名一致(虽然遵循一致是好的实践),但你必须提供一个同名方法。

一个贴近计算的例子

抛开理论,我们看一个更实用的场景:计算百分比。假设我们做一个小工具,需要根据不同的分数制度计算百分比,但所有制度都需要一个基础的分数存储和计算框架。

为此,我们编写一个 Score 抽象类:

abstract class Score {
    protected int $subject1, $subject2, $subject3;

    abstract public function calculatePercentage(): float;
}

Score 类拥有三个受保护的属性来存放科目分数,并声明了一个名为 calculatePercentage() 的抽象方法,要求返回浮点数。至于怎么算,它不管,这是子类的职责。

接着,让 Learner 类去继承并实现它:

class Learner extends Score {
    public function __construct($a, $b, $c) {
        $this->subject1 = $a;
        $this->subject2 = $b;
        $this->subject3 = $c;
    }

    public function calculatePercentage(): float {
        return ($this->subject1 + $this->subject2 + $this->subject3) * 100 / 300;
    }
}

$student = new Learner(50, 60, 70);
echo "Overall Percentage: " . $student->calculatePercentage() . PHP_EOL;
// 输出: Overall Percentage: 60

在这个例子里,Score 抽象类定义了数据结构和能力契约,Learner 类则专注于实现一种特定的百分比算法。如果未来我们需要另一种计算方式,例如加权计算,只需新建一个子类,修改 calculatePercentage() 的实现即可,Score 和 Learner 的代码不用动。

我们为什么需要抽象类?

可能有人会问:普通类加上方法重写不也能实现类似效果吗?为什么非要用抽象类?

答案在于强制性设计意图的表达

  1. 表达意图并施加约束
    当我把一个方法标记为 abstract 时,我在向团队(以及未来的自己)传递一个强烈的信号:“这个方法在每个子类里的行为都不同,你们必须根据自己的场景去实现它,不能依赖父类的任何默认逻辑。” 普通父类可以提供默认实现,子类可以选择覆盖,但抽象方法不具备这个选项,它强制执行。如果不主动使用 abstract 关键词,这个约束在代码层面是缺失的,只能在文档或口头约定里体现,可靠性大打折扣。

  2. 提升代码的组织和复用性
    抽象类像一个高阶的代码收纳盒。把共同的状态(比如 Score 里的 $subject1, $subject2, $subject3)和共用方法(比如通用的 greet() 方法)放在抽象父类里,所有子类就天然拥有了这些能力,避免了在每个子类里重复声明相同的属性或写一遍相同的代码。

抽象类与接口:抉择时刻

这是 PHP 面向对象编程里一个经典的比较。两者都不能直接实例化,都用于定义规范,但它们的能力范围截然不同,选择哪个,取决于你的具体需求。

特性 抽象类 (Abstract Class) 接口 (Interface)
定义方式 使用 abstract 关键词 使用 interface 关键词
实例化 不允许 不允许
方法实现 可以包含已实现的普通方法和未实现的抽象方法 只能声明方法签名,不能有方法体(PHP 8.0后可有默认静态方法)
实现规则 子类必须实现父类中所有抽象方法 实现类必须定义接口中声明的所有方法
属性 可以声明属性,并设定 publicprotectedprivate 可见性 不能声明属性(只能声明常量)
使用场景 多个类之间存在清晰的父子关系,共享代码的同时又要求子类定制特定行为 定义一种与具体实现无关的能力或契约,可以被许多不相关的类采纳

一个我个人比较遵循的原则是:当需要为子类提供一个共同的“骨骼”(比如共享属性、构造方法)时,优先选抽象类。如果只是单纯定义“你能做什么”的行为契约,接口无疑更灵活。

本节课程知识要点

  • 使用 abstract 关键词声明抽象类,它无法直接实例化。

  • 包含抽象方法的,必须声明为抽象类。

  • 继承抽象类的子类,负有实现父类所有抽象方法的合约义务。

  • 通过抽象类,可以集中管理共享的属性和方法,减少代码冗余。

  • 抽象类侧重共同血缘下的代码复用与约束,接口侧重跨类别的能力定义。

← PHP面向对象编程:多态性的核心机制与实战解析 PHP 接口:纯粹的“能力契约” →
分享笔记 (共有 篇笔记)
验证码:
微信公众号