构造函数和析构函数

构造函数

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

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

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

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

<?php
class BaseClass {
function
__construct() {
print
"在 BaseClass 构造函数中\n";
}
}

class
SubClass extends BaseClass {
function
__construct() {
parent::__construct();
print
"在 SubClass 构造函数中\n";
}
}

class
OtherSubClass extends BaseClass {
// 继承 BaseClass 的构造函数
}

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

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

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

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

构造函数是普通的函数,在创建对应对象时调用。因此,它们可以定义任意数量的参数,这些参数可能是必需的,可能具有类型,也可能具有默认值。构造函数参数通过在类名后添加括号中的参数来调用。

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

<?php
class Point {
protected
int $x;
protected
int $y;

public function
__construct(int $x, int $y = 0) {
$this->x = $x;
$this->y = $y;
}
}

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

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

旧式构造函数

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

在命名空间类中,或自 PHP 8.0.0 起的任何类中,与类同名的函数永远不会有任何特殊含义。

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

构造函数提升

自 PHP 8.0.0 起,构造函数参数也可以提升以对应于对象属性。构造函数参数被分配给属性并在构造函数中不再操作的情况非常常见。构造函数提升为这种情况提供了简写。上面的示例可以改写为以下形式。

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

<?php
class Point {
public function
__construct(protected int $x, protected int $y = 0) {
}
}

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

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

注意:

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

注意:

对象属性不能被类型化为 可调用,因为这会引入引擎歧义。因此,提升的参数也不能被类型化为 可调用。但是,任何其他 类型声明 都是允许的。

注意:

由于提升的属性被糖化为属性和函数参数,因此适用于属性和参数的任何命名限制都适用。

注意:

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

初始化器中的 new

自 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() 将阻止剩余的关闭例程执行。

注意:

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

注意:

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

添加说明

用户贡献的说明 15 说明

157
david dot scourfield at llynfi dot co dot uk
13 年前
注意对象内部循环引用引起的潜在内存泄漏。PHP 手册指出“[t]he destructor method will be called as soon as all references to a particular object are removed” 并且这完全正确:如果两个对象相互引用(或者即使一个对象有一个指向自身的字段,如 $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
'Destroying: ', $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 'End of script', PHP_EOL;

?>

这将输出

Destroying: Foo 3
Destroying: Foo 4
End of script
Destroying: Foo 1
Destroying: Foo 2

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

Destroying: Foo 2
Destroying: Foo 1
Destroying: Foo 3
Destroying: Foo 4
End of script

正如预期的那样。

注意:调用 gc_collect_cycles() 会带来速度开销,因此只有在你觉得需要时才使用它。
30
domger at freenet dot de
7 年前
__destruct 魔术方法必须是公共的。

public function __destruct()
{
;
}

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

注意:在 PHP 5.3.10 中,我看到一些 Destructors 被声明为受保护时出现了奇怪的副作用。
2
Hayley Watson
11 个月前
使用静态工厂方法来包装对象构造而不是直接使用构造函数调用还有其他优点。

除了允许在不同场景中使用不同的方法外,对于方法和参数来说,它们都具有更相关的名称,并且构造函数不必处理不同类型参数的不同集合。

* 你可以在尝试构造对象之前进行所有输入验证。
* 对象本身可以在构造其自身类的全新实例时绕过输入验证,因为你可以确保它知道自己在做什么。
* 由于输入验证/预处理被移到工厂方法中,因此构造函数本身通常可以简化为“将这些属性设置为这些参数”,这意味着构造函数提升语法变得更有用。
* 由于对用户隐藏了构造函数,因此它的签名可以更难看一些,而不会让用户感到困扰。呵呵。
* 静态方法可以被提升并作为一等公民的闭包传递,以正常的形式在任何函数可以被调用的地方被调用,而不需要特殊的“new”语法。
* 工厂方法不必返回该精确类的全新实例。它可以返回一个现有的实例,该实例可以完成与新实例相同的工作(在不可变的“值类型”对象的情况下特别有用,通过减少重复);或者返回一个更简单或更具体的子类来完成工作,而比原始类的更通用实例的开销更少。返回子类意味着 LSP 仍然成立。
24
spleen
15 年前
总是容易的事情会让你感到困扰 -

作为 OOP 的新手,我花了相当长的时间才弄清楚在 __construct 这个单词前面有两个下划线。

它是 __construct
不是 _construct

一旦你弄清楚了就非常明显,但直到你弄清楚之前它会让人很沮丧。

我花了很多时间在调试有效的代码上,这完全没有必要。

我甚至思考过这个问题几次,觉得它在示例中看起来有点长,但当时这看起来很愚蠢(总是以为“哦,如果它不是普通的下划线,有人会把它说明白的……”)

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

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

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

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
3
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'。
5
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);
}

}

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

class A
{

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

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

简而言之,当脚本 die() 时,似乎无法预测析构方法的触发顺序。
8
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
7
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 之后发生了关闭。
3
bolshun at mail dot ru
16 年前
确保某个类的实例在另一个类的析构函数中可用很简单:只需在另一个类中保留对该实例的引用即可。
3
Yousef Ismaeil cliprz[At]gmail[Dot]com
10年前
<?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.
1
Jonathon Hibbard
14年前
请注意,在使用 __destruct() 时,你是在 unset 变量……

考虑以下代码
<?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。删除 unset 会解决这个问题。它本来也不需要,因为 PHP 会释放所有内容,但以防你遇到这种情况,你就知道原因了。 ;)
-2
Reza Mahjourian
18年前
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 参数来合并这两个构造函数。
-4
ziggy at start dot dust
1 年前
请注意,构造函数参数提升有点不完整(至少在 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 致命错误:常量表达式包含无效操作”。
-7
instatiendaweb at gmail dot com
3 年前
/**
* 使用两个类和两个析构函数进行测试
* 测试包括在第二个对象的全局变量中访问第一个对象
* 对象的析构函数 2
* 第一个类 ==> $GLOBALS['obj']
* 第二个类 ==> $GLOBALS['obj2']
* 执行构造函数和所有代码...
* 第一个析构函数删除对象并将其设置为 null
* 我们尝试在第二个析构函数中访问 $GLOBALS['obj'],但
* 它不再是一个对象,而是 null
* 警告:在...中未定义的数组键“obj”
*/

class MyDestructableClass{
public $parametro;

function __construct($parametro) {
echo("<div class=\"div\">"), "正在构建 ",__CLASS__ , ("</div>");
escribir::verifacionnota($this ,'在保存变量之前 ');
$this->parametro = $parametro;
escribir::verifacionnota($this ,'在保存变量之后 ');
}



function __destruct() {
escribir::linea(5); // 分隔符
echo("<div class=\"div\">"), "正在销毁 " , __CLASS__ , ("</div>");
escribir::verifacionnota($this ,'在删除变量之前 ');
unset($this->parametro);
escribir::verifacionnota($this ,'在删除变量之后 ');

// unset($GLOBALS[$this]);
}
}

$obj = new MyDestructableClass('parametroone');
escribir::verifacionnota($obj ,' 验证 MyDestructableClass 类,不需要
删除该类,因为它将在脚本结束时执行 ');
escribir::titulosep('在这里可以访问全局变量,正在测试示例');
escribir::verificacion($GLOBALS['obj']);

class destructora{
function __destruct(){
escribir::titulosep('但是该变量将在此处销毁');
escribir::verificacion($GLOBALS['obj']);
}
}

$obj2 = new destructora();
To Top