2024年PHP日本大会

类抽象

PHP具有抽象类、方法和属性。定义为抽象的类不能被实例化,任何包含至少一个抽象方法或属性的类也必须是抽象的。定义为抽象的方法只声明方法的签名以及它是公共的还是受保护的;它们不能定义实现。定义为抽象的属性可以声明对getset行为的要求,并且可以为其中一个操作提供实现,但不能同时为两个操作提供实现。

从抽象类继承时,父类声明中标记为抽象的所有方法都必须由子类定义,并遵循通常的继承签名兼容性规则。

从PHP 8.4开始,抽象类可以声明一个抽象属性,该属性可以是公共的或受保护的。受保护的抽象属性可以通过一个从受保护或公共作用域都可以读取/写入的属性来满足。

抽象属性可以通过标准属性或具有定义的钩子的属性来满足,这对应于所需的操作。

示例 #1 抽象方法示例

<?php

abstract class AbstractClass
{
// 强制扩展类定义此方法
abstract protected function getValue();
abstract protected function
prefixValue($prefix);

// 公共方法
public function printOut()
{
print
$this->getValue() . "\n";
}
}

class
ConcreteClass1 extends AbstractClass
{
protected function
getValue()
{
return
"ConcreteClass1";
}

public function
prefixValue($prefix)
{
return
"{$prefix}ConcreteClass1";
}
}

class
ConcreteClass2 extends AbstractClass
{
public function
getValue()
{
return
"ConcreteClass2";
}

public function
prefixValue($prefix)
{
return
"{$prefix}ConcreteClass2";
}
}

$class1 = new ConcreteClass1();
$class1->printOut();
echo
$class1->prefixValue('FOO_'), "\n";

$class2 = new ConcreteClass2();
$class2->printOut();
echo
$class2->prefixValue('FOO_'), "\n";

?>

以上示例将输出

ConcreteClass1
FOO_ConcreteClass1
ConcreteClass2
FOO_ConcreteClass2

示例 #2 抽象方法示例

<?php

abstract class AbstractClass
{
// 抽象方法只需要定义所需的参数
abstract protected function prefixName($name);
}

class
ConcreteClass extends AbstractClass
{
// 子类可以定义父类签名中不存在的可选参数
public function prefixName($name, $separator = ".")
{
if (
$name == "Pacman") {
$prefix = "Mr";
} elseif (
$name == "Pacwoman") {
$prefix = "Mrs";
} else {
$prefix = "";
}

return
"{$prefix}{$separator} {$name}";
}
}

$class = new ConcreteClass();
echo
$class->prefixName("Pacman"), "\n";
echo
$class->prefixName("Pacwoman"), "\n";

?>

以上示例将输出

Mr. Pacman
Mrs. Pacwoman

示例 #3 抽象属性示例

<?php

abstract class A
{
// 扩展类必须具有一个可公开获取的属性
abstract public string $readable {
get;
}

// 扩展类必须具有一个受保护的或公开的可写入属性
abstract protected string $writeable {
set;
}

// 扩展类必须具有一个受保护的或公开的对称属性
abstract protected string $both {
get;
set;
}
}

class
C extends A
{
// 这满足了要求,也使其可设置,这是有效的
public string $readable;

// 这将不满足要求,因为它不是公开可读的
protected string $readable;

// 这完全满足了要求,因此就足够了。
// 它可能只能写入,并且只能从受保护的作用域写入
protected string $writeable {
set => $value;
}

// 这将可见性从受保护扩展到公共,这是可以的
public string $both;
}

?>

抽象类上的抽象属性可以为任何钩子提供实现,但必须声明getset,但未定义(如上例所示)。

示例 #4 抽象属性示例

<?php

抽象类 A
{
// 此处提供了一个默认(但可重写)的 set 实现,
// 并要求子类提供 get 实现
抽象公共 字符串 $foo {
get;

set {
$this->foo = $value;
}
}
}

?>
添加注释

用户贡献的注释 16条注释

ironiridis at gmail dot com
16年前
再简单解释一次

接口就像一个协议。它不指定对象的行为;它指定你的代码如何告诉对象采取行动。接口就像英语:定义接口定义了你的代码如何与实现该接口的任何对象进行通信。

接口始终是一种协议或承诺。当一个类说“我实现了接口 Y”时,它的意思是“我承诺拥有与任何具有接口 Y 的对象相同的公共方法”。

另一方面,抽象类就像一个部分构建的类。它很像一个带有空白的文档。它可能使用英语,但这并不像文档中已经写了一些内容那么重要。

抽象类是另一个对象的基石。当一个类说“我扩展了抽象类 Y”时,它的意思是“我使用这个名为 Y 的其他类中已经定义的一些方法或属性”。

所以,考虑以下PHP代码
<?php
X 实现 Y { } // 这表示“X”同意使用语言“Y”与你的代码进行通信。

X 扩展 Y { } // 这表示“X”将完成部分类“Y”。
?>

如果你要分发一个供其他人使用的类,你的类应该实现一个特定的接口。接口是对你的类拥有特定一组公共方法的约定。

如果你(或其他人)编写了一个已经编写了一些方法的类,并且你想在你的新类中使用这些方法,那么你的类应该扩展一个抽象类。

这些概念虽然容易混淆,但它们是明确的不同和独立的。就所有意图和目的而言,如果你只是你任何类的唯一用户,你就不需要实现接口。
mbajoras at gmail dot com
14年前
这是一个帮助我理解抽象类的例子。这只是一个非常简单的解释方法(在我看来)。假设我们有以下代码

<?php
水果 {
私有
$颜色;

公共函数
() {
//咀嚼
}

公共函数
设置颜色($c) {
$this->颜色 = $c;
}
}

苹果 扩展 水果 {
公共函数
() {
//咀嚼到果核
}
}

橙子 扩展 水果 {
公共函数
() {
//剥皮
//咀嚼
}
}
?>

现在我给你一个苹果,你吃它。

<?php
$苹果
= new 苹果();
$苹果->();
?>

它尝起来是什么味道?它尝起来像苹果。现在我给你一个水果。

<?php
$水果
= new 水果();
$水果->();
?>

那尝起来是什么味道???好吧,这没有多大意义,所以你不应该这样做。这是通过使水果类和其中的吃方法都成为抽象类来实现的。

<?php
抽象类 水果 {
私有
$颜色;

抽象公共函数
();

公共函数
设置颜色($c) {
$this->颜色 = $c;
}
}
?>

现在考虑一个数据库类,其中 MySQL 和 PostgreSQL 扩展了它。另外,需要注意的是,抽象类就像接口一样,但是你可以在抽象类中定义方法,而在接口中,所有方法都是抽象的。
shewa12kpi at gmail dot com
3年前
<?php
//这是一个抽象类的很好的例子。这里的BaseEmployee不是实际的员工,它只是一个抽象类,它减少了我们的代码并强制子类实现抽象方法

抽象类 BaseEmployee {

/**
* 员工公共属性可以在抽象类中
*/
公共 $名
$姓;

函数
__construct($fn$ln){
$this->= $fn;
$this->= $ln;
}

公共函数
getFullName() {

返回
"$this-> $this->";
}

/**
* 子类必须定义的抽象方法
*/
抽象受保护静态函数 任务();
}

WebDeveloper 扩展 BaseEmployee {

静态函数
任务()
{
返回
'开发网络应用程序';
}
}

人力资源 扩展 BaseEmployee {

静态函数
任务()
{
返回
'管理人力资源';
}

}
/**
* 现在实例化并获取数据
*/
$webDeveloper = new WebDeveloper('shaikh''ahmed');
echo
$webDeveloper->getFullName();
echo
$webDeveloper->任务();
jai at shaped dot ca
7年前
希望这个例子能帮助你了解抽象类是如何工作的,接口是如何工作的,以及它们如何一起工作。此示例也将在 PHP7 上运行/编译,其他示例是在表单中实时键入的,可能有效,但最后一个示例是实际制作/测试的。

<?php

const = PHP_EOL;

// 定义产品*必须*具备的功能(必须实现)
interface productInterface {
public function
doSell();
public function
doBuy();
}

// 定义默认抽象类
abstract class defaultProductAbstraction implements productInterface {
private
$_bought = false;
private
$_sold = false;
abstract public function
doMore();
public function
doSell() {
/* 默认实现 */
$this->_sold = true;
echo
"defaultProductAbstraction doSell: {$this->_sold}".;
}
public function
doBuy() {
$this->_bought = true;
echo
"defaultProductAbstraction doBuy: {$this->_bought}".;
}
}

class
defaultProductImplementation extends defaultProductAbstraction {
public function
doMore() {
echo
"defaultProductImplementation doMore()".;
}
}

class
myProductImplementation extends defaultProductAbstraction {
public function
doMore() {
echo
"myProductImplementation doMore() does more!".;
}
public function
doBuy() {
echo
"myProductImplementation's doBuy() and also my parent's doBuy()".;
parent::doBuy();
}
}

class
myProduct extends defaultProductImplementation {
private
$_bought=true;
public function
__construct() {
var_dump($this->_bought);
}
public function
doBuy () {
/* 非默认doBuy实现 */
$this->_bought = true;
echo
"myProduct overrides the defaultProductImplementation's doBuy() here {$this->_bought}".;
}
}

class
myOtherProduct extends myProductImplementation {
public function
doBuy() {
echo
"myOtherProduct overrides myProductImplementations doBuy() here but still calls parent too".;
parent::doBuy();
}
}

echo
"new myProduct()".;
$product = new myProduct();

$product->doBuy();
$product->doSell();
$product->doMore();

echo
."new defaultProductImplementation()".;

$newProduct = new defaultProductImplementation();
$newProduct->doBuy();
$newProduct->doSell();
$newProduct->doMore();

echo
."new myProductImplementation".;
$lastProduct = new myProductImplementation();
$lastProduct->doBuy();
$lastProduct->doSell();
$lastProduct->doMore();

echo
."new myOtherProduct".;
$anotherNewProduct = new myOtherProduct();
$anotherNewProduct->doBuy();
$anotherNewProduct->doSell();
$anotherNewProduct->doMore();
?>

运行结果
<?php
/*
new myProduct()
bool(true)
myProduct overrides the defaultProductImplementation's doBuy() here 1
defaultProductAbstraction doSell: 1
defaultProductImplementation doMore()

new defaultProductImplementation()
defaultProductAbstraction doBuy: 1
defaultProductAbstraction doSell: 1
defaultProductImplementation doMore()

new myProductImplementation
myProductImplementation's doBuy() and also my parent's doBuy()
defaultProductAbstraction doBuy: 1
defaultProductAbstraction doSell: 1
myProductImplementation doMore() does more!

new myOtherProduct
myOtherProduct overrides myProductImplementations doBuy() here but still calls parent too
myProductImplementation's doBuy() and also my parent's doBuy()
defaultProductAbstraction doBuy: 1
defaultProductAbstraction doSell: 1
myProductImplementation doMore() does more!

*/
?>
a dot tsiaparas at watergate dot gr
13年前
抽象类和接口是两种截然不同的工具,它们就像锤子和钻头一样。抽象类可以包含已实现的方法,而接口本身没有任何实现。

将所有方法都声明为抽象的抽象类,并不是名称不同的接口。可以实现多个接口,但不能扩展多个类(或抽象类)。

抽象类与接口的使用取决于具体问题,选择是在软件设计阶段做出的,而不是在实现阶段。在同一个项目中,你也可以提供一个接口和一个作为参考的基类(可能是抽象类)来实现该接口。你为什么要这么做呢?

假设我们要构建一个调用不同服务的系统,而这些服务又具有不同的操作。通常,我们可以提供一个名为execute的方法,它接受操作名称作为参数并执行该操作。

我们要确保类能够定义自己执行操作的方式。因此,我们创建一个名为IService的接口,其中包含execute方法。但在大多数情况下,你将复制粘贴完全相同的execute代码。

我们可以为名为Service的类创建一个参考实现并实现execute方法。这样,其他类就不需要再复制粘贴代码了!但是,如果你想扩展MySLLi呢?你可以实现接口(可能需要复制粘贴),然后你又得到了一个服务。抽象类可以在类中包含初始化代码,而这些代码不能为每个你将编写的类预定义。

希望这不会太令人费解,并对某些人有所帮助。干杯!
Alexios Tsiaparas
swashata4u at gmail dot com
6年前
这是关于抽象类和接口的另一个方面。

有时,我们为`Factory`定义一个接口,并通过`abstract`类简化`Factory`的一些常用方法。

在这种情况下,抽象类实现了接口,但不一定需要实现接口的所有方法。

简单的理由是,任何实现接口的类都需要实现所有方法,或者声明自己为抽象类。

因此,以下代码是完全正确的。

<?php
接口 Element {
/**
* 构造函数。必须传入现有的配置,或者对于新元素保留原样,届时将使用默认值。
*
* @param array $config 元素配置。
*/
public function __construct( $config = [] );

/**
* 获取元素的定义。
*
* @return array 包含 'title'、'description' 和 'type' 的数组
*/
public static function get_definition();

/**
* 获取元素配置变量。
*
* @return array 元素配置的关联数组。
*/
public function get_config();

/**
* 设置元素配置变量。
*
* @param array $config 新的配置变量。
*
* @return void
*/
public function set_config( $config );
}

抽象类
Base 实现 Element {

/**
* 元素配置变量
*
* @var array
*/
protected $config = [];

/**
* 获取元素配置变量。
*
* @return array 元素配置的关联数组。
*/
public function get_config() {
return
$this->config;
}

/**
* 创建一个 eForm 元素实例
*
* @param array $config 元素配置。
*/
public function __construct( $config = [] ) {
$this->set_config( $config );
}
}

MyElement 扩展 Base {

public static function
get_definition() {
return [
'type' => 'MyElement',
];
}

public function
set_config( $config ) {
// 在此处执行某些操作
$this->config = $config;
}
}

$element = new MyElement( [
'foo' => 'bar',
] );

print_r( $element->get_config() );
?>

您可以看到此处执行的测试,PHP 5.4 及更高版本,输出一致。https://3v4l.org/8NqqW
shaman_master at list dot ru
6年前
您还可以为抽象方法声明返回/参数类型 (PHP>=7.0)
<?php
声明(strict_types=1);

抽象类
Adapter
{
protected
$name;
abstract public function
getName(): string;
abstract public function
setName(string $value);
}

AdapterFoo 扩展 Adapter
{
public function
getName(): string
{
return
$this->name;
}
// 在抽象类中未定义返回类型声明,在此处设置
public function setName(string $value): self
{
$this->name = $value;
return
$this;
}
}
?>
Eugeny at Kostanay dot KZ
8年前
一段代码片段,帮助您更多地了解抽象类中的属性
<?php
抽象类 anotherAbsClass
{
// 定义并设置静态属性
static $stProp = 'qwerty'; // 我们仍然可以通过静态方式直接使用它
// 定义并设置受保护属性
protected $prProp = 'walrus';
// 为抽象类的非静态变量设置任何其他可见性级别都是无用的。
// 我们甚至无法在抽象类的声明方法内访问私有属性,因为我们无法在对象上下文中调用该方法。
// 公共方法的实现
protected function callMe() {
echo
'On call: ' . $this->prProp . PHP_EOL;
}
// 一些抽象方法的声明
abstract protected function abc($arg1, $arg2);
abstract public function
getJunk($arg1, $arg2, $arg3, $junkCollector = true);
// 注意:如果抽象类已声明可选值,则我们不能省略它而不会出错
}

someChildClass 扩展 anotherAbsClass
{
function
__construct() {
echo
$this->callMe() . PHP_EOL; // 现在我们从抽象类中继承了受保护的属性 $prProp
}
// 必须在下面实现已声明的函数 abc 和 getJunk
protected function abc($val1, $val) {
// 执行某些操作
}
function
getJunk($val1, $val2, $val3, $b = false) { // 需要可选值,因为它已在上面声明
// 执行某些操作
}
}

echo
anotherAbsClass::$stProp; // qwerty
$objTest = new someChildClass; // On call: walrus
?>
joelhy
13年前
文档说明:“不允许创建已定义为抽象的类的实例”。这仅意味着您无法从抽象类初始化对象。调用抽象类的静态方法仍然是可行的。例如
<?php
抽象类 Foo
{
static function
bar()
{
echo
"test\n";
}
}

Foo::bar();
?>
sneakyimp at hotmail dot com
17年前
好的……当涉及到扩展另一个抽象类的抽象类时,文档有点含糊不清。扩展另一个抽象类的抽象类不需要定义父类的抽象方法。换句话说,这会导致错误

<?php
抽象类 class1 {
abstract public function
someFunc();
}
抽象类
class2 扩展 class1 {
abstract public function
someFunc();
}
?>

错误:致命错误:无法继承抽象函数 class1::someFunc()(先前在 class2 中声明为抽象)位于 /home/sneakyimp/public/chump.php 的第 7 行

但是这不会

<?php
抽象类 class1 {
abstract public function
someFunc();
}
抽象类
class2 扩展 class1 {
}
?>

扩展抽象类的抽象类可以在实现其父抽象类的抽象方法时将责任传递给其子类。
bishop
14年前
顺便说一句,抽象类不需要是基类

<?php
Foo {
public function
sneeze() { echo 'achoooo'; }
}

抽象类
Bar 继承 Foo {
public abstract function
hiccup();
}

Baz 继承 Bar {
public function
hiccup() { echo 'hiccup!'; }
}

$baz = new Baz();
$baz->sneeze();
$baz->hiccup();
?>
jai at shaped dot ca
7年前
接口指定了一个类必须实现的方法,这样任何使用该类并期望其遵守该接口的代码都能正常工作。

例如:我希望任何 `$database` 对象都具有 `->doQuery()` 方法,因此我分配给数据库接口的任何类都应该实现 `databaseInterface` 接口,该接口强制实现 `doQuery` 方法。

<?php
接口 dbInterface {
public function
doQuery();
}

myDB 实现 dbInterface {
public function
doQuery() {
/* 实现细节在此处 */
}
}

$myDBObj = new myDB()->doQuery();
?>

抽象类类似,只是可以预定义一些方法。列为抽象的方法必须像接口一样进行定义。

例如:我希望我的 `$person` 对象能够执行 `->doWalk()` 方法,大多数人用两条腿走路没问题,但有些人不得不跳着走 :(

<?php
接口 PersonInterface() {
/* 每个人都应该走路,或尝试走路 */
public function doWalk($place);
/* 每个人都应该能够变老 */
public function doAge();
}

抽象类
AveragePerson 实现 PersonInterface() {
private
$_age = 0;
public function
doAge() {
$this->_age = $this->_age+1;
}
public function
doWalk($place) {
echo
"我将步行到 $place".PHP_EOL;
}
/* 每个人说话都不一样! */
abstract function talk($say);
}

Joe 继承 AveragePerson {
public function
talk($say) {
echo
"用澳大利亚口音,Joe 说: $say".PHP_EOL;
}
}

Bob 继承 AveragePerson {
public function
talk($say) {
echo
"用加拿大口音,Bob 说: $say".PHP_EOL;
}
public function
doWalk($place) {
echo
"Bob 只有一条腿,必须跳到 $place".PHP_EOL;
}
}

$people[] = new Bob();
$people[] = new Joe();

foreach (
$people as $person) {
$person->doWalk('那边');
$person->talk('PHP 规则');
}
?>
joebert
17年前
我不同意 jfkallens 关于抽象类和对象接口的最后比较。

在抽象类中,你可以定义一些方法的工作方式,而在对象接口中则不行。

对象接口本质上只是一列函数名,如果一个类实现了该接口,则必须定义这些函数名。

抽象类本质上是一个原型,暗示了扩展类应该做什么。
抽象类也可以被认为是一个提供一些基本功能的基类,并且还定义了一个内置的对象接口,所有扩展类都将实现该接口。

因此,对象接口实际上是抽象类的内置部分。
Malcolm
8年前
我发现示例 #2 抽象类示例中存在不一致。

如果删除 `$separator` 的默认值

<?php
public function prefixName($name, $separator) {
// ...
}
?>

那么 php 将显示此致命错误消息
致命错误:ConcreteClass::prefixName() 的声明必须与 AbstractClass::prefixName($name) 兼容,位于 /index.php 的第 23 行

奇怪的是,它给出了 "ConcreteClass::prefixName()" 的不正确声明……它缺少两个参数。因此,我认为这只是一个错误,可能在新版本中已经解决了。(或者只针对我的版本)我主要提到这一点是因为它在我的某些测试代码中让我非常抓狂,这些测试代码是从示例 #2(没有额外参数的默认值)派生的。也许这可以为其他人节省一些不必要的麻烦。

--
请注意,我在 php5.5 上运行此代码。
操作系统:ubuntu-16.04-server-amd64.iso
仓库:ppa:ondrej/php

# php5.5 --version
PHP 5.5.36-2+donate.sury.org~xenial+1 (cli)
版权所有 (c) 1997-2015 PHP Group
Zend Engine v2.5.0,版权所有 (c) 1998-2015 Zend Technologies,带有 Zend OPcache v7.0.6-dev,版权所有 (c) 1999-2015,由 Zend Technologies 提供
arma99eDAN at yahoo dot com
9 年前
你也可以这样使用抽象类

抽象类 A{
public function show(){
echo 'A';
}
}
类 B 继承 A{
public function hello(){
echo 'B';
parent::show();
}
}

$obj = new B;
$obj->hello(); // BA
# 请注意,该抽象类至少不包含一个抽象方法
# 即使在这种情况下,我仍然能够扩展它,或调用其非抽象成员
designbyjeeba at gmail dot com
13年前
请注意父字段的可见性。如果字段是私有的,那么你将无法在子类中看到这些字段。这是基本的 OOP 原则,但有时可能会带来问题。
To Top