1
0
mirror of https://github.com/php/doc-zh.git synced 2026-03-23 22:52:08 +01:00
Files
archived-doc-zh/language/oop5/basic.xml
2025-11-10 11:01:45 +08:00

808 lines
20 KiB
XML
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?xml version="1.0" encoding="utf-8"?>
<!-- $Revision$ -->
<!-- EN-Revision: fc068b4857342eb409efeafe6723058f39ab1045 Maintainer: avenger Status: ready -->
<!-- CREDITS: mowangjuanzi, Luffy -->
<sect1 xml:id="language.oop5.basic" xmlns="http://docbook.org/ns/docbook">
<title>基本概念</title>
<sect2 xml:id="language.oop5.basic.class">
<title>class</title>
<para>
每个类的定义都以关键字 <literal>class</literal>
开头,后面跟着类名,后面跟着一对花括号,里面包含有类的属性与方法的定义。
</para>
<para>
类名可以是任何不是 PHP <link linkend="reserved">保留字</link>
的有效标签。自 PHP 8.4.0 起,弃用使用单个下划线 <literal>_</literal>
作为类名。有效类名以字母或下划线开头,后面跟着若干字母、数字或下划线。以正则表达式表示为
<code>^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$</code>
</para>
<para>
一个类可以包含有属于自己的 <link
linkend="language.oop5.constants">常量</link><link
linkend="language.oop5.properties">变量</link>(称为“属性”)以及函数(称为“方法”)。
</para>
<example>
<title>简单的类定义</title>
<programlisting role="php">
<![CDATA[
<?php
class SimpleClass
{
// 声明属性
public $var = 'a default value';
// 声明方法
public function displayVar() {
echo $this->var;
}
}
?>
]]>
</programlisting>
</example>
<para>
当一个方法在类定义内部被调用时,有一个可用的伪变量
<varname>$this</varname><varname>$this</varname>
是一个到当前对象的引用。
</para>
<warning>
<para>
以静态方式去调用一个非静态方法,将会抛出一个
<classname>Error</classname>
在 PHP 8.0.0 之前版本中,将会产生一个废弃通知,同时
<varname>$this</varname> 将会被声明为未定义。
</para>
<example xml:id="language.oop5.basic.class.this">
<title>使用 <varname>$this</varname> 伪变量的示例</title>
<programlisting role="php">
<![CDATA[
<?php
class A
{
function foo()
{
if (isset($this)) {
echo '$this is defined (';
echo get_class($this);
echo ")\n";
} else {
echo "\$this is not defined.\n";
}
}
}
class B
{
function bar()
{
A::foo();
}
}
$a = new A();
$a->foo();
A::foo();
$b = new B();
$b->bar();
B::bar();
?>
]]>
</programlisting>
&example.outputs.7;
<screen>
<![CDATA[
$this is defined (A)
Deprecated: Non-static method A::foo() should not be called statically in %s on line 27
$this is not defined.
Deprecated: Non-static method A::foo() should not be called statically in %s on line 20
$this is not defined.
Deprecated: Non-static method B::bar() should not be called statically in %s on line 32
Deprecated: Non-static method A::foo() should not be called statically in %s on line 20
$this is not defined.
]]>
</screen>
&example.outputs.8;
<screen>
<![CDATA[
$this is defined (A)
Fatal error: Uncaught Error: Non-static method A::foo() cannot be called statically in %s :27
Stack trace:
#0 {main}
thrown in %s on line 27
]]>
</screen>
</example>
</warning>
<sect3 xml:id="language.oop5.basic.class.readonly">
<title>只读类</title>
<para>
自 PHP 8.2.0 起,可以使用 <modifier>readonly</modifier> 修饰符来标记类。将类标记为 <modifier>readonly</modifier>
只会向每个声明的属性添加 <link linkend="language.oop5.properties.readonly-properties"><modifier>readonly</modifier>
修饰符</link>并禁止创建<link linkend="language.oop5.properties.dynamic-properties">动态属性</link>。此外,不能通过使用
<classname>AllowDynamicProperties</classname> 注解来添加对后者的支持。尝试这样做会触发编译错误。
</para>
<informalexample>
<programlisting role="php">
<![CDATA[
<?php
#[\AllowDynamicProperties]
readonly class Foo {
}
// Fatal error: Cannot apply #[AllowDynamicProperties] to readonly class Foo
?>
]]>
</programlisting>
</informalexample>
<para>
由于无类型的属性和静态属性不能用 <literal>readonly</literal> 修饰符,所以 readonly 也不会对其声明:
</para>
<informalexample>
<programlisting role="php">
<![CDATA[
<?php
readonly class Foo
{
public $bar;
}
// Fatal error: Readonly property Foo::$bar must have type
?>
]]>
</programlisting>
<programlisting role="php">
<![CDATA[
<?php
readonly class Foo
{
public static int $bar;
}
// Fatal error: Readonly class Foo cannot declare static properties
?>
]]>
</programlisting>
</informalexample>
<para>
仅当子类也是 <modifier>readonly</modifier> 类时,才可以<link
linkend="language.oop5.basic.extends">继承</link> <modifier>readonly</modifier> 类。
</para>
</sect3>
</sect2>
<sect2 xml:id="language.oop5.basic.new">
<title>new</title>
<para>
要创建一个类的实例,必须使用 <literal>new</literal>
关键字。当创建新对象时该对象总是被赋值,除非该对象定义了 <link
linkend="language.oop5.decon">构造函数</link> 并且在出错时抛出了一个 <link
linkend="language.exceptions">异常</link>。类应在被实例化之前定义(某些情况下则必须这样)。
</para>
<para>
如果一个变量包含一个类名的 <type>string</type><literal>new</literal> 时,将创建该类的一个新实例。
如果该类属于一个命名空间,则必须使用其完整名称。
</para>
<note>
<para>
如果没有参数要传递给类的构造函数,类名后的括号则可以省略掉。
</para>
</note>
<example>
<title>创建实例</title>
<programlisting role="php">
<![CDATA[
<?php
class SimpleClass {
}
$instance = new SimpleClass();
var_dump($instance);
// 也可以这样做:
$className = 'SimpleClass';
$instance = new $className(); // new SimpleClass()
var_dump($instance);
?>
]]>
</programlisting>
</example>
<para>
PHP 8.0.0 起,支持任意表达式中使用 <literal>new</literal>。如果表达式生成一个
<type>string</type>,这将允许更复杂的实例化。表达式必须使用括号括起来。
</para>
<example>
<title>使用任意表达式创建实例</title>
<para>
在下列示例中我们展示了多个生成类名的任意有效表达式的示例。展示了函数调用string 连接和 <constant>::class</constant> 常量。
</para>
<programlisting role="php">
<![CDATA[
<?php
class ClassA extends \stdClass {}
class ClassB extends \stdClass {}
class ClassC extends ClassB {}
class ClassD extends ClassA {}
function getSomeClass(): string
{
return 'ClassA';
}
var_dump(new (getSomeClass()));
var_dump(new ('Class' . 'B'));
var_dump(new ('Class' . 'C'));
var_dump(new (ClassD::class));
?>
]]>
</programlisting>
&example.outputs.8;
<screen>
<![CDATA[
object(ClassA)#1 (0) {
}
object(ClassB)#1 (0) {
}
object(ClassC)#1 (0) {
}
object(ClassD)#1 (0) {
}
]]>
</screen>
</example>
<para>
在类定义内部,可以用 <literal>new self</literal><literal>new parent</literal> 创建新对象。
</para>
<para>
当把一个对象已经创建的实例赋给一个新变量时,新变量会访问同一个实例,就和用该对象赋值一样。此行为和给函数传递入实例时一样。可以用
<link linkend="language.oop5.cloning">克隆</link> 给一个已创建的对象建立一个新实例。
</para>
<example>
<title>对象赋值</title>
<programlisting role="php">
<![CDATA[
<?php
class SimpleClass {
public string $var;
}
$instance = new SimpleClass();
$assigned = $instance;
$reference =& $instance;
$instance->var = '$assigned will have this value';
$instance = null; // $instance 和 $reference 变为 null
var_dump($instance);
var_dump($reference);
var_dump($assigned);
?>
]]>
</programlisting>
&example.outputs;
<screen>
<![CDATA[
NULL
NULL
object(SimpleClass)#1 (1) {
["var"]=>
string(30) "$assigned will have this value"
}
]]>
</screen>
</example>
<para>
有几种方法可以创建一个对象的实例。
</para>
<example>
<title>创建新对象</title>
<programlisting role="php">
<![CDATA[
<?php
class Test
{
public static function getNew()
{
return new static();
}
}
class Child extends Test {}
$obj1 = new Test(); // 通过类名
$obj2 = new $obj1(); // 通过包含对象的变量
var_dump($obj1 !== $obj2);
$obj3 = Test::getNew(); // 通过类方法
var_dump($obj3 instanceof Test);
$obj4 = Child::getNew(); // 通过子类方法
var_dump($obj4 instanceof Child);
?>
]]>
</programlisting>
&example.outputs;
<screen>
<![CDATA[
bool(true)
bool(true)
bool(true)
]]>
</screen>
</example>
<para>
可以通过一个表达式来访问新创建对象的成员:
</para>
<example>
<title>访问新创建对象的成员</title>
<programlisting role="php">
<![CDATA[
<?php
echo (new DateTime())->format('Y'), PHP_EOL;
// 从 PHP 8.4.0 起,周围的括号是可选的
echo new DateTime()->format('Y'), PHP_EOL;
?>
]]>
</programlisting>
&example.outputs.similar;
<screen>
<![CDATA[
2025
2025
]]>
</screen>
</example>
<note>
<simpara>
在 PHP 7.1 之前,如果类没有定义构造函数,则不对参数进行执行。
</simpara>
</note>
</sect2>
<sect2 xml:id="language.oop5.basic.properties-methods">
<title>属性和方法</title>
<para>
类的属性和方法存在于不同的“命名空间”中,这意味着同一个类的属性和方法可以使用同样的名字。
在类中访问属性和调用方法使用同样的操作符,具体是访问一个属性还是调用一个方法,取决于你的上下文,即用法是变量访问还是函数调用。
</para>
<example>
<title>访问类属性 vs. 调用类方法</title>
<programlisting role="php">
<![CDATA[
<?php
class Foo
{
public $bar = 'property';
public function bar() {
return 'method';
}
}
$obj = new Foo();
echo $obj->bar, PHP_EOL, $obj->bar(), PHP_EOL;
]]>
</programlisting>
&example.outputs;
<screen>
<![CDATA[
property
method
]]>
</screen>
</example>
<para>
这意味着,如果你的类属性被分配给一个
<link linkend="functions.anonymous">匿名函数</link>
你将无法直接调用它。因为访问类属性的优先级要更高,在此场景下需要用括号包裹起来调用。
</para>
<example>
<title>类属性被赋值为匿名函数时的调用示例</title>
<programlisting role="php">
<![CDATA[
<?php
class Foo
{
public $bar;
public function __construct() {
$this->bar = function() {
return 42;
};
}
}
$obj = new Foo();
echo ($obj->bar)(), PHP_EOL;
]]>
</programlisting>
&example.outputs;
<screen>
<![CDATA[
42
]]>
</screen>
</example>
</sect2>
<sect2 xml:id="language.oop5.basic.extends">
<!-- TODO Example about class constant redefinition -->
<!-- TODO Split into it's own page? -->
<title>extends</title>
<para>
一个类可以在声明中用 <literal>extends</literal>
关键字继承另一个类的方法和属性。PHP 不支持多重继承,一个类只能继承一个基类。
</para>
<para>
被继承的方法和属性可以通过用同样的名字重新声明被覆盖。但是如果父类定义方法或者常量时使用了
<link linkend="language.oop5.final">final</link>,则不可被覆盖。可以通过
<link linkend="language.oop5.paamayim-nekudotayim">parent::</link>
来访问被覆盖的方法或属性。
</para>
<note>
<simpara>
从 PHP 8.1.0 起,常量可以声明为 final。
</simpara>
</note>
<example>
<title>简单的类继承</title>
<programlisting role="php">
<![CDATA[
<?php
class SimpleClass
{
function displayVar()
{
echo "Parent class\n";
}
}
class ExtendClass extends SimpleClass
{
// 同样名称的方法,将会覆盖父类的方法
function displayVar()
{
echo "Extending class\n";
parent::displayVar();
}
}
$extended = new ExtendClass();
$extended->displayVar();
?>
]]>
</programlisting>
&example.outputs;
<screen>
<![CDATA[
Extending class
Parent Class
]]>
</screen>
</example>
<sect3 xml:id="language.oop.lsp">
<title>签名兼容性规则</title>
<para>
当覆盖override方法时签名必须兼容父类方法。否则会导致 Fatal 错误PHP 8.0.0 之前是 <constant>E_WARNING</constant> 级错误。
兼容签名是指:遵守<link linkend="language.oop5.variance">协变与逆变</link>规则强制参数可以改为可选参数添加的新参数只能是可选放宽可见性而不是继续限制。这就是著名的里氏替换原则Liskov
Substitution Principle简称 LSP。不过<link linkend="language.oop5.decon.constructor">构造方法</link>和私有(<literal>private</literal>)方法不需要遵循签名兼容规则,哪怕签名不匹配也不会导致 Fatal 错误。
</para>
<example>
<title>兼容子类方法</title>
<programlisting role="php">
<![CDATA[
<?php
class Base
{
public function foo(int $a) {
echo "Valid\n";
}
}
class Extend1 extends Base
{
function foo(int $a = 5)
{
parent::foo($a);
}
}
class Extend2 extends Base
{
function foo(int $a, $b = 5)
{
parent::foo($a);
}
}
$extended1 = new Extend1();
$extended1->foo();
$extended2 = new Extend2();
$extended2->foo(1);
]]>
</programlisting>
&example.outputs;
<screen>
<![CDATA[
Valid
Valid
]]>
</screen>
</example>
<para>
下面演示子类与父类方法不兼容的例子:通过移除参数、修改可选参数为必填参数。
</para>
<example>
<title>子类方法移除参数后,导致 Fatal 错误</title>
<programlisting role="php">
<![CDATA[
<?php
class Base
{
public function foo(int $a = 5) {
echo "Valid\n";
}
}
class Extend extends Base
{
function foo()
{
parent::foo(1);
}
}
]]>
</programlisting>
&example.outputs.8.similar;
<screen>
<![CDATA[
Fatal error: Declaration of Extend::foo() must be compatible with Base::foo(int $a = 5) in /in/evtlq on line 13
]]>
</screen>
</example>
<example>
<title>子类方法把可选参数改成强制参数,导致 Fatal 错误</title>
<programlisting role="php">
<![CDATA[
<?php
class Base
{
public function foo(int $a = 5) {
echo "Valid\n";
}
}
class Extend extends Base
{
function foo(int $a)
{
parent::foo($a);
}
}
]]>
</programlisting>
&example.outputs.8.similar;
<screen>
<![CDATA[
Fatal error: Declaration of Extend::foo(int $a) must be compatible with Base::foo(int $a = 5) in /in/qJXVC on line 13
]]>
</screen>
</example>
<warning>
<para>
重命名子类方法的参数名称也是签名兼容的。
然而我们不建议这样做,因为使用<link linkend="functions.named-arguments">命名参数</link>时,
这种做法会导致运行时的 <classname>Error</classname>
</para>
<example>
<title>在子类中重命一个命名参数,导致 Error</title>
<programlisting role="php">
<![CDATA[
<?php
class A {
public function test($foo, $bar) {}
}
class B extends A {
public function test($a, $b) {}
}
$obj = new B;
// 按 A::test() 的签名约定传入参数
$obj->test(foo: "foo", bar: "bar"); // ERROR!
]]>
</programlisting>
&example.outputs.similar;
<screen>
<![CDATA[
Fatal error: Uncaught Error: Unknown named parameter $foo in /in/XaaeN:14
Stack trace:
#0 {main}
thrown in /in/XaaeN on line 14
]]>
</screen>
</example>
</warning>
</sect3>
</sect2>
<sect2 xml:id="language.oop5.basic.class.class">
<title>::class</title>
<para>
关键词 <literal>class</literal> 也可用于类名的解析。使用 <literal>ClassName::class</literal>
可以获取包含类 <literal>ClassName</literal> 的完全限定名称。这对使用了
<link linkend="language.namespaces">命名空间</link> 的类尤其有用。
</para>
<para>
<example xml:id="language.oop5.basic.class.class.name">
<title>类名的解析</title>
<programlisting role="php">
<![CDATA[
<?php
namespace NS {
class ClassName {
}
echo ClassName::class;
}
?>
]]>
</programlisting>
&example.outputs;
<screen>
<![CDATA[
NS\ClassName
]]>
</screen>
</example>
</para>
<note>
<para>使用 <literal>::class</literal>
解析类名操作会在底层编译时进行。这意味着在执行该操作时,类还没有被加载。
因此,即使要调用的类不存在,类名也会被展示。在此种场景下,并不会发生错误。
</para>
<example xml:id="language.oop5.basic.class.class.fail">
<title>解析不存在的类名</title>
<programlisting role="php">
<![CDATA[
<?php
print Does\Not\Exist::class;
?>
]]>
</programlisting>
&example.outputs;
<screen>
<![CDATA[
Does\Not\Exist
]]>
</screen>
</example>
</note>
<para>
自 PHP 8.0.0 起,<literal>::class</literal> 也可用于对象。
与上述情况不同,此时解析将会在运行时进行。此操作的运行结果和在对象上调用
<function>get_class</function> 相同。
</para>
<example xml:id="language.oop5.basic.class.class.object">
<title>类名解析</title>
<programlisting role="php">
<![CDATA[
<?php
namespace NS {
class ClassName {
}
$c = new ClassName();
print $c::class;
}
?>
]]>
</programlisting>
&example.outputs;
<screen>
<![CDATA[
NS\ClassName
]]>
</screen>
</example>
</sect2>
<sect2 xml:id="language.oop5.basic.nullsafe">
<title>Nullsafe 方法和属性</title>
<para>
自 PHP 8.0.0 起,类属性和方法可以通过 "nullsafe" 操作符访问:
<literal>?-></literal>
除了一处不同nullsafe 操作符和以上原来的属性、方法访问是一致的:
对象引用解析dereference&null; 时不抛出异常,而是返回 &null;
并且如果是链式调用中的一部分,剩余链条会直接跳过。
</para>
<para>
此操作的结果,类似于在每次访问前使用 <function>is_null</function>
函数判断方法和属性是否存在,但更加简洁。
</para>
<para>
<example>
<title>Nullsafe 操作符</title>
<programlisting role="php" annotations="non-interactive">
<![CDATA[
<?php
// 自 PHP 8.0.0 起可用
$result = $repository?->getUser(5)?->name;
// 上边那行代码等价于以下代码
if (is_null($repository)) {
$result = null;
} else {
$user = $repository->getUser(5);
if (is_null($user)) {
$result = null;
} else {
$result = $user->name;
}
}
?>
]]>
</programlisting>
</example>
</para>
<note>
<para>
仅当 null 被认为是属性或方法返回的有效和预期的可能值时,才推荐使用 nullsafe
操作符。如果业务中需要明确指示错误,抛出异常会是更好的处理方式。
</para>
</note>
</sect2>
</sect1>
<!-- Keep this comment at the end of the file
Local variables:
mode: sgml
sgml-omittag:t
sgml-shorttag:t
sgml-minimize-attributes:nil
sgml-always-quote-attributes:t
sgml-indent-step:1
sgml-indent-data:t
indent-tabs-mode:nil
sgml-parent-document:nil
sgml-default-dtd-file:"~/.phpdoc/manual.ced"
sgml-exposed-tags:nil
sgml-local-catalogs:nil
sgml-local-ecat-files:nil
End:
vim600: syn=xml fen fdm=syntax fdl=2 si
vim: et tw=78 syn=sgml
vi: ts=1 sw=1
-->