PHP Conference Japan 2024

构造函数和析构函数

构造函数

__construct(混合 ...$values = ""):

PHP 允许开发者为类声明构造函数方法。具有构造函数方法的类会在每个新创建的对象上调用此方法,因此它适用于对象在使用前可能需要的任何初始化。

注意: 如果子类定义了构造函数,则不会隐式调用父构造函数。为了运行父构造函数,需要在子构造函数中调用 parent::__construct()。如果子类没有定义构造函数,那么它可以像普通类方法一样从父类继承(如果它没有声明为私有)。

示例 #1 继承中的构造函数

<?php
BaseClass {
函数
__construct() {
打印
"在 BaseClass 构造函数中\n";
}
}

SubClass 扩展 BaseClass {
函数
__construct() {
parent::__construct();
打印
"在 SubClass 构造函数中\n";
}
}

OtherSubClass 扩展 BaseClass {
// 继承 BaseClass 的构造函数
}

// 在 BaseClass 构造函数中
$obj = 新的 BaseClass();

// 在 BaseClass 构造函数中
// 在 SubClass 构造函数中
$obj = 新的 SubClass();

// 在 BaseClass 构造函数中
$obj = 新的 OtherSubClass();
?>

与其他方法不同,__construct() 在被扩展时不受通常的 签名兼容性规则 的约束。

构造函数是普通方法,在实例化其对应对象期间调用。因此,它们可以定义任意数量的参数,这些参数可能是必需的,可能具有类型,并且可能具有默认值。通过在类名后括号中放置参数来调用构造函数参数。

示例 #2 使用构造函数参数

<?php
Point {
受保护的
int $x;
受保护的
int $y;

公共函数
__construct(int $x, int $y = 0) {
$this->x = $x;
$this->y = $y;
}
}

// 传递两个参数。
$p1 = 新的 Point(4, 5);
// 只传递必需的参数。$y 将取其默认值 0。
$p2 = 新的 Point(4);
// 使用命名参数(从 PHP 8.0 开始):
$p3 = 新的 Point(y: 5, x: 4);
?>

如果类没有构造函数,或者构造函数没有必需的参数,则可以省略括号。

旧式构造函数

在 PHP 8.0.0 之前,全局命名空间中的类会将与类名称相同的名称的方法解释为旧式构造函数。该语法已弃用,并将导致 E_DEPRECATED 错误,但仍会将该函数作为构造函数调用。如果同时定义了 __construct() 和同名方法,则将调用 __construct()

在命名空间类中,或从 PHP 8.0.0 开始的任何类中,与类名称相同的名称的方法没有任何特殊含义。

在新代码中始终使用 __construct()

构造函数提升

从 PHP 8.0.0 开始,构造函数参数也可以提升以对应于对象属性。构造函数参数分配给属性并在构造函数中不进行其他操作的情况非常常见。构造函数提升为此用例提供了一种简写方式。上面的示例可以改写如下。

示例 #3 使用构造函数属性提升

<?php
Point {
公共函数
__construct(受保护的 int $x, 受保护的 int $y = 0) {
}
}

当构造函数参数包含修饰符时,PHP 会将其解释为对象属性和构造函数参数,并将参数值分配给属性。然后,构造函数体可以为空,也可以包含其他语句。任何其他语句都将在参数值已分配给相应属性后执行。

并非所有参数都需要提升。可以混合使用提升和未提升的参数,并且可以按任何顺序排列。提升的参数对调用构造函数的代码没有影响。

注意:

使用 可见性修饰符publicprotectedprivate)是最可能应用属性提升的方法,但任何其他单个修饰符(例如 readonly)也将具有相同的效果。

注意:

由于引擎歧义性可能带来的问题,对象属性不能被类型化为callable。因此,提升的参数也不能被类型化为callable。但是,任何其他类型声明都是允许的。

注意:

由于提升的属性会被转换为属性和函数参数,因此属性和参数的所有命名限制都适用。

注意:

放置在提升的构造函数参数上的属性将被复制到属性和参数。提升的构造函数参数上的默认值将仅复制到参数,而不是属性。

初始化器中的新特性

从 PHP 8.1.0 开始,对象可以作为默认参数值、静态变量和全局常量使用,以及在属性参数中使用。现在,对象也可以传递给define() 函数。

注意:

不允许使用动态或非字符串类名或匿名类。不允许使用参数解包。不允许使用不受支持的表达式作为参数。

示例 #4 在初始化器中使用 new

<?php

// 所有都允许:
static $x = new Foo;

const
C = new Foo;

function
test($param = new Foo) {}

#[
AnAttribute(new Foo)]
class
Test {
public function
__construct(
public
$prop = new Foo,
) {}
}

// 所有都不允许(编译时错误):
function test(
$a = new (CLASS_NAME_CONSTANT)(), // 动态类名
$b = new class {}, // 匿名类
$c = new A(...[]), // 参数解包
$d = new B($abc), // 不受支持的常量表达式
) {}
?>

静态创建方法

PHP 每个类只支持一个构造函数。但是,在某些情况下,可能需要允许以不同的方式使用不同的输入来构造对象。建议的方法是使用静态方法作为构造函数包装器。

示例 #5 使用静态创建方法

<?php
class Product {

private ?
int $id;
private ?
string $name;

private function
__construct(?int $id = null, ?string $name = null) {
$this->id = $id;
$this->name = $name;
}

public static function
fromBasicData(int $id, string $name): static {
$new = new static($id, $name);
return
$new;
}

public static function
fromJson(string $json): static {
$data = json_decode($json, true);
return new static(
$data['id'], $data['name']);
}

public static function
fromXml(string $xml): static {
// 自定义逻辑。
$data = convert_xml_to_array($xml);
$new = new static();
$new->id = $data['id'];
$new->name = $data['name'];
return
$new;
}
}

$p1 = Product::fromBasicData(5, 'Widget');
$p2 = Product::fromJson($some_json_string);
$p3 = Product::fromXml($some_xml_string);

构造函数可以被设为私有或受保护,以防止它被外部调用。如果是这样,只有静态方法才能实例化类。因为它们在同一个类定义中,所以即使不是同一个对象实例,它们也可以访问私有方法。私有构造函数是可选的,根据用例的不同,它可能是有意义的,也可能没有意义。

然后,三个公共静态方法演示了实例化对象的不同方法。

  • fromBasicData() 采用所需的精确参数,然后通过调用构造函数并返回结果来创建对象。
  • fromJson() 接受一个 JSON 字符串,并对其进行一些预处理,将其转换为构造函数所需的格式。然后返回新对象。
  • fromXml() 接受一个 XML 字符串,对其进行预处理,然后创建一个裸对象。构造函数仍然会被调用,但由于所有参数都是可选的,因此该方法会跳过它们。然后,它直接为对象属性赋值,然后再返回结果。

在所有三种情况下,static 关键字都会转换为代码所在类的名称。在本例中,为 Product

析构函数

__destruct(): void

PHP 拥有类似于其他面向对象语言(如 C++)的析构函数概念。一旦没有其他对特定对象的引用,或者在关闭序列中的任何顺序,析构函数方法将被调用。

示例 #6 析构函数示例

<?php

class MyDestructableClass
{
function
__construct() {
print
"In constructor\n";
}

function
__destruct() {
print
"Destroying " . __CLASS__ . "\n";
}
}

$obj = new MyDestructableClass();

与构造函数一样,引擎不会隐式调用父析构函数。为了运行父析构函数,必须在析构函数体中显式调用parent::__destruct()。同样,与构造函数一样,如果子类本身没有实现析构函数,则它可以继承父类的析构函数。

即使使用exit() 停止脚本执行,也会调用析构函数。exit() 在析构函数中调用将阻止剩余的关闭例程执行。

如果析构函数创建了对自身对象的新引用,则当引用计数再次达到零或在关闭序列期间,它不会被再次调用。

从 PHP 8.4.0 开始,当循环收集Fiber执行期间发生时,计划要收集的对象的析构函数将在一个单独的 Fiber 中执行,称为 gc_destructor_fiber。如果此 Fiber 被挂起,将创建一个新的 Fiber 来执行任何剩余的析构函数。以前的 gc_destructor_fiber 将不再被垃圾回收器引用,如果它在其他地方没有被引用,则可以被收集。其析构函数被挂起的对象将不会被收集,直到析构函数返回或 Fiber 本身被收集。

注意:

在脚本关闭期间调用的析构函数已经发送了 HTTP 标头。在脚本关闭阶段,某些 SAPI(例如 Apache)的工作目录可能不同。

注意:

尝试从析构函数(在脚本终止时调用)抛出异常会导致致命错误。

添加注释

用户贡献的注释 14 条注释

david dot scourfield at llynfi dot co dot uk
13 年前
请注意对象内部循环引用引起的潜在内存泄漏。PHP 手册指出“一旦删除对特定对象的所有引用,就会调用析构函数方法”,这正是事实:如果两个对象相互引用(或者如果一个对象有一个指向自身的字段,如 $this->foo = $this),那么此引用将阻止调用析构函数,即使根本没有其他对该对象的引用。程序员无法再访问这些对象,但它们仍然驻留在内存中。

请考虑以下示例

<?php

header
("Content-type: text/plain");

class
Foo {

/**
* 标识符
* @var string
*/
private $name;
/**
* 另一个 Foo 对象的引用
* @var Foo
*/
private $link;

public function
__construct($name) {
$this->name = $name;
}

public function
setLink(Foo $link){
$this->link = $link;
}

public function
__destruct() {
echo
'销毁: ', $this->name, PHP_EOL;
}
}

// 创建两个 Foo 对象:
$foo = new Foo('Foo 1');
$bar = new Foo('Foo 2');

// 使它们相互引用
$foo->setLink($bar);
$bar->setLink($foo);

// 销毁对它们的全局引用
$foo = null;
$bar = null;

// 我们现在无法访问 Foo 1 或 Foo 2,因此它们应该被 __destruct() 销毁
// 但它们没有被销毁,因此由于它们仍然在内存中,所以会出现内存泄漏。
//
// 取消注释下一行以查看显式调用 GC 时的区别:
// gc_collect_cycles();
//
// 另请参阅: https://php.net/manual/en/features.gc.php
//

// 创建另外两个 Foo 对象,但不要设置它们的内部 Foo 引用
// 所以除了变量 $foo 和 $bar 之外,没有任何东西指向它们:
$foo = new Foo('Foo 3');
$bar = new Foo('Foo 4');

// 销毁对它们的全局引用
$foo = null;
$bar = null;

// 我们现在无法访问 Foo 3 或 Foo 4,并且由于没有任何其他引用
// 指向它们,因此它们的 __destruct() 方法在此处自动调用,
// 在执行下一行之前:

echo '脚本结束', PHP_EOL;

?>

这将输出

销毁: Foo 3
销毁: Foo 4
脚本结束
销毁: Foo 1
销毁: Foo 2

但是,如果我们在脚本中间取消注释 gc_collect_cycles(); 函数调用,我们将得到

销毁: Foo 2
销毁: Foo 1
销毁: Foo 3
销毁: Foo 4
脚本结束

可能需要。

注意:调用 gc_collect_cycles() 会产生速度开销,因此仅在您认为需要时才使用它。
Hayley Watson
1 年前
使用静态工厂方法包装对象构造而不是裸构造函数调用还有其他优点。

除了允许在不同场景中使用不同的方法外,还可以为方法和参数提供更相关的名称,并且构造函数不必处理不同类型不同参数集。

* 您可以在尝试构造对象之前执行所有输入验证。
* 对象本身可以在构造其自身类的新实例时绕过该输入验证,因为您可以确保它知道自己在做什么。
* 通过将输入验证/预处理移动到工厂方法,构造函数本身通常可以简化为“将这些属性设置为这些参数”,这意味着构造函数提升语法变得更有用。
* 由于已隐藏在用户之外,构造函数的签名可以稍微难看一些,而不会给他们带来麻烦。呵呵。
* 静态方法可以提升并作为一等闭包传递,以便在函数可以调用的任何地方以常规方式调用,而无需特殊的“new”语法。
* 工厂方法不必返回该确切类的新的实例。它可以返回一个预先存在的实例,该实例可以完成新实例将执行的相同工作(尤其是在不可变“值类型”对象的情况下,通过减少重复);或者返回一个更简单或更具体的子类,以较少的开销完成工作,而不是原始类的更通用实例。返回子类意味着 LSP 仍然有效。
domger at freenet dot de
7 年前
__destruct 魔术方法必须是公有的。

public function __destruct()
{
;
}

该方法将自动在实例外部被调用。将 __destruct 声明为受保护或私有的将导致警告,并且不会调用魔术方法。

注意:在 PHP 5.3.10 中,我看到一些析构函数声明为受保护时出现奇怪的副作用。
spleen
16 年前
总是容易的事情会让你陷入困境 -

作为 OOP 新手,我花了好一阵子才弄清楚在 __construct 这个词前面有两个下划线。

它是 __construct
而不是 _construct

一旦你弄清楚了,就非常明显,但在你弄清楚之前,它可能会非常令人沮丧。

我花了很多不必要的时间调试工作代码。

我甚至想过几次,觉得在示例中看起来有点长,但当时这看起来很愚蠢(总是在想“如果它不是普通的下划线,有人会把它说清楚的……”)

我查看的所有手册,我阅读的所有教程,我浏览的所有示例 - 没有一次有人提到过这一点!

(请不要告诉我它在这个页面上的某个地方有解释,而我只是错过了它,你只会增加我的痛苦。)

我希望这对其他人有所帮助!
iwwp at outlook dot com
4 年前
为了更好地理解 __destrust 方法

class A {
protected $id;

public function __construct($id)
{
$this->id = $id;
echo "construct {$this->id}\n";
}

public function __destruct()
{
echo "destruct {$this->id}\n";
}
}

$a = new A(1);
echo "-------------\n";
$aa = new A(2);
echo "=============\n";

输出内容

construct 1
-------------
construct 2
=============
destruct 2
destruct 1
mmulej at gmail dot com
2 年前
*<重复发布> 我无法编辑我之前的笔记以详细说明修饰符。请原谅我。*

如果父类和子类都定义了同名方法,并且在父类的构造函数中调用它,则使用 `parent::__construct()` 将调用子类中的方法。

<?php

class A {
public function
__construct() {
$this->method();
}
public function
method() {
echo
'A' . PHP_EOL;
}
}
class
B extends A {
public function
__construct() {
parent::__construct();
}
}
class
C extends A {
public function
__construct() {
parent::__construct();
}
public function
method() {
echo
'C' . PHP_EOL;
}
}
$b = new B; // A
$c = new C; // C

?>

在此示例中,A::method 和 C::method 都是公有的。

您可以将 A::method 更改为受保护的,并将 C::method 更改为受保护的或公有的,它仍然可以正常工作。

但是,如果您将 A::method 设置为私有的,那么 C::method 是私有的、受保护的还是公有的都没关系。$b 和 $c 都将回显 'A'。
david at synatree dot com
16 年前
当脚本正在执行 die() 时,您无法依赖 __destruct() 的调用顺序。

对于我一直在处理的一个脚本,我想对任何传出的数据进行透明的低级加密。为此,我使用了如下配置的全局单例类

class EncryptedComms
{
private $C;
private $objs = array();
private static $_me;

public static function destroyAfter(&$obj)
{
self::getInstance()->objs[] =& $obj;
/*
希望通过强制另一个对象的引用存在
在这个类中,引用的对象需要在
垃圾回收发生在此对象上之前销毁。这将强制
此对象的析构方法在所有
此处引用的对象的析构函数之后触发。
*/
}
public function __construct($key)
{
$this->C = new SimpleCrypt($key);
ob_start(array($this,'getBuffer'));
}
public static function &getInstance($key=NULL)
{
if(!self::$_me && $key)
self::$_me = new EncryptedComms($key);
else
return self::$_me;
}

public function __destruct()
{
ob_end_flush();
}

public function getBuffer($str)
{
return $this->C->encrypt($str);
}

}

在这个例子中,我尝试注册其他对象,以便始终在该对象之前销毁。像这样

类 A
{

public function __construct()
{
EncryptedComms::destroyAfter($this);
}
}

人们可能会认为单例中包含的对象的引用会首先被销毁,但事实并非如此。事实上,即使您反转范式并在您希望在之前销毁的每个对象中存储对 EncryptedComms 的引用,这也不会起作用。

简而言之,当脚本 die() 时,似乎无法预测析构函数将按什么顺序触发。
prieler at abm dot at
17 年前
我编写了一个关于 php 5.2.1 中析构函数和关闭函数顺序的快速示例

<?php
class destruction {
var
$name;

function
destruction($name) {
$this->name = $name;
register_shutdown_function(array(&$this, "shutdown"));
}

function
shutdown() {
echo
'shutdown: '.$this->name."\n";
}

function
__destruct() {
echo
'destruct: '.$this->name."\n";
}
}

$a = new destruction('a: global 1');

function
test() {
$b = new destruction('b: func 1');
$c = new destruction('c: func 2');
}
test();

$d = new destruction('d: global 2');

?>

这将输出
shutdown: a: global 1
shutdown: b: func 1
shutdown: c: func 2
shutdown: d: global 2
destruct: b: func 1
destruct: c: func 2
destruct: d: global 2
destruct: a: global 1

结论
析构函数始终在脚本结束时调用。
析构函数按其“上下文”顺序调用:首先是函数,然后是全局对象
函数上下文中的对象按设置顺序删除(较旧的对象优先)。
全局上下文中的对象按相反顺序删除(较旧的对象最后)。

关闭函数在析构函数之前调用。
关闭函数按其“注册”顺序调用。;)

此致,J
Per Persson
12 年前
从 PHP 5.3.10 开始,由致命错误引起的关闭时不会运行析构函数。

例如
<?php
class Logger
{
protected
$rows = array();

public function
__destruct()
{
$this->save();
}

public function
log($row)
{
$this->rows[] = $row;
}

public function
save()
{
echo
'<ul>';
foreach (
$this->rows as $row)
{
echo
'<li>', $row, '</li>';
}
echo
'</ul>';
}
}

$logger = new Logger;
$logger->log('Before');

$nonset->foo();

$logger->log('After');
?>

如果没有 $nonset->foo(); 行,则 Before 和 After 都将打印,但如果有该行,则两者都不会打印。

但是,可以将析构函数或其他方法注册为关闭函数
<?php
class Logger
{
protected
$rows = array();

public function
__construct()
{
register_shutdown_function(array($this, '__destruct'));
}

public function
__destruct()
{
$this->save();
}

public function
log($row)
{
$this->rows[] = $row;
}

public function
save()
{
echo
'<ul>';
foreach (
$this->rows as $row)
{
echo
'<li>', $row, '</li>';
}
echo
'</ul>';
}
}

$logger = new Logger;
$logger->log('Before');

$nonset->foo();

$logger->log('After');
?>
现在将打印 Before,但不会打印 After,因此您可以看到在 Before 之后发生了关闭。
bolshun at mail dot ru
16 年前
确保某个类的实例将在另一个类的析构函数中可用很容易:只需在另一个类中保留对该实例的引用即可。
Yousef Ismaeil cliprz[At]gmail[Dot]com
11 年前

<?php

/**
* 一个有趣的 Mobile 类示例
*
* @author Yousef Ismaeil Cliprz[At]gmail[Dot]com
*/

class Mobile {

/**
* 一些设备属性
*
* @var string
* @access public
*/
public $deviceName,$deviceVersion,$deviceColor;

/**
* 为 Mobile::properties 设置一些值
*
* @param string 设备名称
* @param string 设备版本
* @param string 设备颜色
*/
public function __construct ($name,$version,$color) {
$this->deviceName = $name;
$this->deviceVersion = $version;
$this->deviceColor = $color;
echo
"The ".__CLASS__." class is stratup.<br /><br />";
}

/**
* 一些输出
*
* @access public
*/
public function printOut () {
echo
'I have a '.$this->deviceName
.' version '.$this->deviceVersion
.' my device color is : '.$this->deviceColor;
}

/**
* 嗯,仅仅作为示例,我们将移除 Mobile::$deviceName 嗯,不是 unset,只是为了检查 __destruct 的工作方式
*
* @access public
*/
public function __destruct () {
$this->deviceName = 'Removed';
echo
'<br /><br />Dumpping Mobile::deviceName to make sure its removed, Olay :';
var_dump($this->deviceName);
echo
"<br />The ".__CLASS__." class is shutdown.";
}

}

// 哦,是的实例
$mob = new Mobile('iPhone','5','Black');

// 打印输出
$mob->printOut();

?>

The Mobile class is stratup.

I have a iPhone version 5 my device color is : Black

Dumpping Mobile::deviceName to make sure its removed, Olay
string 'Removed' (length=7)

The Mobile class is shutdown.
Jonathon Hibbard
14 years ago
请注意,在使用 __destruct() 时,您何时取消设置变量...

考虑以下代码
<?php
class my_class {
public
$error_reporting = false;

function
__construct($error_reporting = false) {
$this->error_reporting = $error_reporting;
}

function
__destruct() {
if(
$this->error_reporting === true) $this->show_report();
unset(
$this->error_reporting);
}
?>

以上将导致错误
Notice: Undefined property: my_class::$error_reporting in my_class.php on line 10

看起来变量会在 if 语句实际执行之前被取消设置。删除 unset 将解决此问题。无论如何都不需要它,因为 PHP 也会释放所有内容,但以防万一您遇到这种情况,您就知道原因了;)
Reza Mahjourian
18 years ago
Peter 建议使用静态方法来弥补 PHP 中多个构造函数的不可用性。这在大多数情况下都能正常工作,但是如果您有一个类层次结构并且希望将初始化的部分委托给父类,那么您将无法再使用此方案。这是因为与构造函数不同,在静态方法中,您需要自己进行实例化。因此,如果您调用父静态方法,您将获得一个父类型的对象,您无法继续使用派生类字段对其进行初始化。

假设您有一个 Employee 类和一个派生的 HourlyEmployee 类,并且您希望能够从某些 XML 输入中构建这些对象。

<?php
class Employee {
public function
__construct($inName) {
$this->name = $inName;
}

public static function
constructFromDom($inDom)
{
$name = $inDom->name;
return new
Employee($name);
}

private
$name;
}

class
HourlyEmployee extends Employee {
public function
__construct($inName, $inHourlyRate) {
parent::__construct($inName);
$this->hourlyRate = $inHourlyRate;
}

public static function
constructFromDom($inDom)
{
// can't call parent::constructFromDom($inDom)
// need to do all the work here again
$name = $inDom->name; // increased coupling
$hourlyRate = $inDom->hourlyrate;
return new
EmployeeHourly($name, $hourlyRate);
}

private
$hourlyRate;
}
?>

唯一的解决方案是将两个构造函数合并为一个,方法是在每个构造函数中添加一个可选的 $inDom 参数。
ziggy at start dot dust
2 年前
请注意,构造函数参数提升有点半生不熟(至少在 8.1 之前是这样,并且在 8.2 中似乎没有改变),并且不允许您将提升的参数与其他提升的参数重复使用。例如,拥有“旧样式”构造函数

<?php
public function __construct(protected string $val, protected Foo $foo = null) {
$this->val = $val;
$this->foo = $foo ?? new Foo($val);
}
?>

您将无法像这样使用参数提升

<?php
public function __construct(protected string $val, protected Foo $foo = new Foo($val)) {}
?>

也不

<?php
public function __construct(protected string $val, protected Foo $foo = new Foo($this->val)) {}
?>

因为在这两种情况下,您都会遇到“PHP Fatal error: Constant expression contains invalid operations”。
To Top