PHP 类和对象
PHP 中的对象并不会像简单数据类型(比如数字、字符串)那样直接复制数据,而是类似于传递一个地址,这个地址指向内存中的对象。因此,所有引用这个地址的变量,其实都操作的是同一个对象。
基本概念
class
在 PHP 中,用 class 关键字来定义一个类。一个类就是一个“模板”或“蓝图”,描述了某种类型的对象应该具有什么属性(变量)和方法(函数)。
类名可以用字母或下划线开头,后面可以是字母、数字或下划线,但不能用 PHP 的保留字(比如 class, function, if 等等)。合法的类名的规则类似正则表达式:^[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*$
class Car {
// 属性(变量)
public $brand;
// 方法(函数)
public function drive() {
echo "The car is driving";
}
}
只读类
PHP 8.2.0 允许你将一个类标记为 readonly,这意味着该类中的所有属性都会自动变成只读的,不能在赋值后修改。
一旦类被标记为 readonly,你不能动态添加属性(即运行时给对象增加新的属性)。即使你使用 #[AllowDynamicProperties] 注解,也是不允许的,会报错。
示例:
readonly class Car {
public string $brand;
}
$car = new Car();
$car->color = 'red'; // 错误!不能添加动态属性
readonly 类的主要作用是让类的所有属性都变成只能赋值一次的只读属性,同时不允许动态添加新属性。在 readonly 类中,无类型声明的属性和静态属性不能使用 readonly 修饰符。因此,readonly 类中也不能定义这些属性。
new 关键字
new 关键字用于创建类的实例(对象)。当你使用 new 来实例化一个类时,PHP 会调用该类的构造函数(__construct())来初始化对象。
定义类:
class Car {
public $brand;
public $color;
// 构造函数,用于初始化对象时传递参数
public function __construct($brand, $color) {
$this->brand = $brand;
$this->color = $color;
}
// 一个类的方法
public function displayInfo() {
echo "This is a {$this->color} {$this->brand}.";
}
}
使用 new 创建对象:
$myCar = new Car("Toyota", "red"); // 实例化 Car 类
$myCar->displayInfo(); // 输出: This is a red Toyota.
属性和方法
在 PHP 类中,属性和方法位于不同的命名空间,因此可以使用相同的名称。而 PHP 会根据具体的上下文判断当前是访问属性还是调用方法。例如,在以下例子中,bar 既是类的属性,又是类的方法。
class Foo
{
public $bar = 'property'; // 定义类的属性 bar
public function bar() { // 定义类的方法 bar
return 'method';
}
}
$obj = new Foo();
echo $obj->bar, PHP_EOL, $obj->bar(), PHP_EOL;
• 访问属性:当你使用 $obj->bar,表示你是在访问属性 bar。
• 调用方法:当你使用 $obj->bar(),表示你是在调用方法 bar()。
extends
extends
是 PHP 中用来实现继承的关键字。 继承就像是一种“继承财产”的概念,一个类(子类)可以继承另一个类(父类)的属性和方法。这样,子类就可以直接使用父类已经定义好的功能,而不需要重新编写。
<?php
class ExtendClass extends SimpleClass
{
// 同样名称的方法,将会覆盖父类的方法
function displayVar()
{
echo "Extending class\n";
parent::displayVar();
}
}
$extended = new ExtendClass();
$extended->displayVar();
?>
以上示例会输出:
Extending class
a default value
::class
在PHP中,::class
是一个特殊的语法,它用来获取一个类的全名(包括命名空间)。这在处理类的名称时非常有用,特别是当你的项目结构中包含了多个命名空间的时候。
当你需要获取一个类的全名时,可以使用 ::class
。例如:
echo \NS\ClassName::class; // 输出 "NS\ClassName"
这里 \NS\ClassName::class
会返回字符串 "NS\ClassName"
,其中 NS
是命名空间的名字。
当使用 ::class
来获取类名时,这个操作是在编译期间完成的。这意味着即使类尚未被加载或根本不存在,你仍然可以获取到类名。如果类不存在,PHP 不会抛出错误,只会返回一个不存在的类名。
print Does\Not\Exist::class; // 输出 "Does\Not\Exist"
从PHP 8.0起,你也可以在一个对象上使用 ::class
来获取它的类名,这时解析将在运行时进行。这实际上与调用 get_class()
方法的效果是一样的。
namespace NS;
class ClassName {
// 类的具体实现
}
$c = new ClassName(); // 创建一个 ClassName 的实例
print $c::class; // 输出 "NS\ClassName"
Nullsafe 方法和属性
在 PHP 中,当你尝试访问一个对象的方法或者属性时,如果你使用的对象是 null
,那么程序就会抛出一个错误。但是有了 ?->
这个操作符之后,即使对象是 null
,也不会报错,而是默默地返回 null
。
示例:
假设我们有一个 $repository
对象,我们想从中获取用户的名称。如果 $repository
或者获取到的用户是 null
,我们可以这样做:
// 使用 Nullsafe 操作符
$result = $repository?->getUser(5)?->name;
等价的传统写法:
如果你不用 ?->
,你需要写很多 if
语句来检查是否为 null
:
// 传统写法
if ($repository === null) {
$result = null;
} else {
$user = $repository->getUser(5);
if ($user === null) {
$result = null;
} else {
$result = $user->name;
}
}
虽然 Nullsafe 操作符让代码看起来更简洁,但它并不总是最佳选择。如果在你的应用程序逻辑中,某个对象或方法应该是存在的,而不存在是一个错误情况,那么应该让程序抛出异常而不是返回 null
。
属性
在 PHP 中,类属性就像是一个类的成员或组成部分。你可以把它们想象成存储在类里的数据盒子。当你创建类的一个实例(也就是一个对象)时,每个实例都会有自己的这些属性副本。
声明类属性时,需要指定一些规则,告诉 PHP 这个属性的特性是什么。这些规则包括:
- 访问控制:决定谁可以访问这个属性。比如
public
表示任何人都可以访问;private
表示只有类内部能访问;protected
表示类及其子类可以访问。 - 是否静态:
static
关键字表示这个属性是属于整个类的,而不是某个特定对象的。 - 只读属性:自 PHP 8.1 开始,可以使用
readonly
关键字来声明一个不能被外部修改的属性。 - 类型声明:自 PHP 7.4 起,可以在声明属性时指定其类型,例如
int
或string
。
这些规则不是必须都写上的,除了 readonly
必须要有外,其他的都可以省略。如果没有明确指定访问控制,那么属性默认是 public
的。
初始化属性
可以给属性一个初始值。这个初始值必须是一个常量表达式,比如一个字符串、数字或者是常量。
访问属性
对于普通(非静态)属性,可以通过 $this->propertyName
的形式来访问,这里 $this
是指向当前对象的一个引用。
对于静态属性,则使用 ClassName::$propertyName
来访问。
声明属性示例:
<?php
class SimpleClass {
// 公开的字符串属性
public $var1 = 'hello world';
// 公开的多行字符串属性
public $var2 = <<<EOD
hello world
EOD;
// 公开的整数属性
public $var3 = 1 + 2;
// 常量值初始化
public $var6 = 'myConstant'; // 注意:这里的 'myConstant' 是一个字符串值,并不是一个真正的常量
// 数组初始化
public $var7 = [true, false];
// 字符串内联表达式
public $var8 = <<<'EOD'
hello world
EOD;
// 没有访问控制修饰符,默认为 public
static $var9;
// 只读属性,自 PHP 8.1 起支持
readonly int $var10;
}
// 创建一个 SimpleClass 的实例
$object = new SimpleClass();
echo $object->var1; // 输出 "hello world"
?>
类常量
如果你有一些值在程序运行期间不会改变,那么你可以把这些值定义为“常量”。
类常量的定义很简单,只需要在类内部使用 const
关键字,后跟常量名称以及等号和常量的初始值即可。例如:
class MyClass {
const MY_CONSTANT = 'Hello, World!';
}
可以通过类名直接访问类常量,而不需要创建类的实例对象。例如:
echo MyClass::MY_CONSTANT; // 输出 'Hello, World!'
类常量的可见性
从 PHP 7.1.0 版本开始,类常量可以有可见性修饰符(public, protected, private)。但是,默认情况下它们是 public
的,可以在任何地方访问它们。不过,在 PHP 8.1.0 及更高版本中,可以使用 final
关键字来防止子类覆盖父类中的常量。
使用 ::class
::class
是一个特殊的常量,它返回完全限定的类名。这在处理命名空间时特别有用。例如:
echo MyClass::class; // 输出 'MyClass' 或带有命名空间的完整类名
类的自动加载
在编写面向对象(OOP) 程序时,很多开发者为每个类新建一个 PHP 文件。这样做有一个小问题:每次你需要使用这些类时,都得在主脚本的最开始部分用include
或require
语句把所有相关的文件引入进来。如果你有很多类,这个列表可能会变得非常长,而且难以管理。
PHP提供了一个叫做“自动加载”的功能来帮助我们解决这个问题。它的工作原理是这样的:当你试图使用一个还未被定义的类时,PHP会在代码报错之前,先尝试加载这个类所在的文件。
实现自动加载的核心函数是spl_autoload_register()
。你可以使用这个函数来告诉PHP,“如果我使用了一个没有定义的类,那么请帮我加载这个类”。
不仅是类,像是接口、trait(特质)以及枚举这样的结构也可以用同样的方式自动加载。
注意:在PHP 8.0之前,可通过__autoload
的方法来实现自动加载,但是这种方法不够灵活,已经被废弃了,在新版本的PHP中不再支持。
示例:
// 注册一个自动加载函数
spl_autoload_register(function ($className) {
// 将类名转换成文件名,并尝试包含该文件
require_once $className . '.php';
});
// 创建两个类的实例
$obj = new MyClass1();
$obj2 = new MyClass2();
当我们创建MyClass1
和MyClass2
的实例时,如果没有预先引入它们的文件,PHP会自动调用注册好的自动加载函数,尝试加载MyClass1.php
和MyClass2.php
文件。
构造函数和析构函数
构造函数
在 PHP 中,你可以创建一种特殊的函数叫做构造函数。这个函数的名字必须和类名相同,它会在每次创建一个新对象时自动运行。适合用来在对象使用之前做一些准备工作,比如初始化属性值。
有几点需要注意:
• 如果一个类有父类,并且子类中定义了构造函数,PHP 不会自动调用父类的构造函数。你需要在子类的构造函数中手动调用 parent::__construct()
,这样父类的构造函数才会被执行。
• 如果子类没有构造函数,PHP 会像普通方法一样继承父类的构造函数(前提是父类的构造函数没有被定义为 private)。
析构函数
PHP 中的析构函数,它在对象生命周期结束时执行,用来释放资源或执行清理操作。析构函数会在某个对象的所有引用都被删除,或者对象被显式销毁时调用。
示例:
<?php
class MyDestructableClass
{
// 构造函数
function __construct() {
print "In constructor\n"; // 对象创建时执行
}
// 析构函数
function __destruct() {
print "Destroying " . __CLASS__ . "\n"; // 对象销毁时执行
}
}
// 创建对象实例
$obj = new MyDestructableClass();
// 脚本结束时或者显式销毁对象时,析构函数将被调用
析构函数在脚本结束时被调用,此时如果尝试抛出异常,会导致致命错误。
访问控制(可见性)
PHP 类中的属性和方法可以设置三种访问级别:
- public(公开): 任何地方都能访问
- protected(受保护): 只能在当前类和子类中访问
- private(私有): 只能在定义它的类内部访问
属性访问控制
class Book {
public $title = "PHP教程"; // 公开属性,任何地方都能访问
protected $author = "张三"; // 受保护属性,只能在当前类和子类中访问
private $price = 29.9; // 私有属性,只能在当前类中访问
}
$book = new Book();
echo $book->title; // 正常运行
echo $book->author; // 错误!不能访问protected属性
echo $book->price; // 错误!不能访问private属性
方法访问控制
class Animal {
public function eat() { // 公开方法
echo "吃东西";
}
protected function sleep() { // 受保护方法
echo "睡觉";
}
private function drink() { // 私有方法
echo "喝水";
}
}
$cat = new Animal();
$cat->eat(); // 正常运行
$cat->sleep(); // 错误!不能访问protected方法
$cat->drink(); // 错误!不能访问private方法
继承中的访问控制
class Animal {
protected $name = "动物";
}
class Cat extends Animal {
public function getName() {
echo $this->name; // 可以访问父类的protected属性
}
}
不写访问控制关键字时,默认为 public。
对象继承
继承允许子类继承父类的特性:
- 子类会继承父类所有 public 和 protected 的属性和方法
- private 成员不会被继承
- 子类可以覆盖(重写)父类的方法
class Animal {
protected $name;
public function eat() {
echo "动物在吃东西";
}
}
// Dog 继承 Animal
class Dog extends Animal {
public function bark() {
echo "汪汪叫";
}
// 重写父类方法
public function eat() {
echo "狗在吃骨头";
}
}
$dog = new Dog();
$dog->eat(); // 输出:狗在吃骨头
$dog->bark(); // 输出:汪汪叫
范围解析操作符 (::)
双冒号(::)用于访问:
- 静态成员(属性和方法)
- 类常量
- 被重写的父类方法
class Example {
const VERSION = '1.0';
public static $count = 0;
public static function hello() {
echo "Hello";
}
}
// 访问方式
echo Example::VERSION; // 访问常量
echo Example::$count; // 访问静态属性
Example::hello(); // 调用静态方法
在类内部可以使用三个特殊关键字:
class Child extends Parent {
public static $name = 'child';
public function test() {
self::$name; // 访问当前类
parent::method(); // 访问父类
static::$name; // 后期静态绑定
}
}
//self: 表示当前类
//parent: 表示父类
//static: 表示运行时最终调用的类
静态(static)关键字
声明类属性或方法为静态,就可以不实例化类而直接访问。可以在实例化的类对象中通过静态访问。
静态属性:
class User {
public static $count = 0; // 静态属性
public function __construct() {
self::$count++; // 计数器
}
}
new User();
new User();
echo User::$count; // 输出: 2
// 正确的访问方式
User::$count; // 通过类名
self::$count; // 在类内部
$user::$count; // 通过实例(不推荐)
// 错误的访问方式
$user->count; // 错误:不能用对象操作符
$user->$count; // 错误:不能用对象操作符
静态方法:
class Math {
public static function add($a, $b) {
return $a + $b;
}
public static function multiply($a, $b) {
// 静态方法中不能使用 $this
return $a * $b;
}
}
// 调用静态方法
echo Math::add(2, 3); // 输出: 5
echo Math::multiply(2, 3); // 输出: 6
抽象类
在PHP中,抽象类是一种不能直接实例化的类。换句话说,你不能用 new 关键字来创建一个抽象类的对象。抽象类的主要作用是作为其他类的基类,定义出一系列子类必须实现的模板。这样的设计可以确保所有子类都包含某些关键方法,保持代码结构一致。
示例:
<?php
// 定义一个抽象类
abstract class AbstractClass
{
// 抽象方法(没有具体实现)
abstract protected function getValue();
abstract protected function prefixValue($prefix);
// 普通方法(非抽象方法)
public function printOut() {
// 调用子类的 getValue 方法
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(); // 输出 "ConcreteClass1"
echo $class1->prefixValue('FOO_') . "\n"; // 输出 "FOO_ConcreteClass1"
$class2 = new ConcreteClass2;
$class2->printOut(); // 输出 "ConcreteClass2"
echo $class2->prefixValue('FOO_') . "\n"; // 输出 "FOO_ConcreteClass2"
?>
对象接口
在PHP中,接口(interface)是一个用于定义类结构的工具。接口定义了类需要实现哪些方法,但不提供这些方法的实现细节。这使得接口成为一种规范,确保不同类可以共享某些特性并提供一致的接口。
接口使用 interface 关键字来定义,并且接口中的方法必须是 public。
示例:
<?php
interface LoggerInterface
{
public function log(string $message): void;
}
class FileLogger implements LoggerInterface
{
public function log(string $message): void {
echo "Logging to a file: " . $message;
}
}
class DatabaseLogger implements LoggerInterface
{
public function log(string $message): void {
echo "Logging to a database: " . $message;
}
}
// 使用 FileLogger
$logger = new FileLogger();
$logger->log("File log example.");
// 使用 DatabaseLogger
$logger = new DatabaseLogger();
$logger->log("Database log example.");
?>
在上面的例子中,LoggerInterface 定义了一个 log 方法,FileLogger 和 DatabaseLogger 类都实现了该接口。这样可以确保所有实现 LoggerInterface 的类都具备 log 方法。
Trait
Trait 是一种代码复用机制,专为单继承语言设计,用于解决传统多继承中的问题。它允许开发者将一组功能封装在 Trait 中,并灵活地应用到不同的类中,而不需要通过继承。
示例:
<?php
trait Logger {
public function log($message) {
echo "[Log]: $message\n";
}
}
trait Notifier {
public function notify($message) {
echo "[Notify]: $message\n";
}
}
// 使用 Trait 的类
class User {
use Logger, Notifier; // 引入两个 Trait
public function createUser($name) {
$this->log("User '$name' created.");
$this->notify("Welcome, $name!");
}
}
$user = new User();
$user->createUser("Alice");
输出:
[Log]: User 'Alice' created.
[Notify]: Welcome, Alice!
匿名类
匿名类(Anonymous Class)是 PHP 7 引入的一种语言特性。它允许开发者直接定义没有名字的类,并在定义时实例化。匿名类特别适用于只需要一次性使用或临时实现的类,从而减少显式定义类的冗余代码。
显示定义类:
<?php
class Logger {
public function log($msg) {
echo $msg;
}
}
$util->setLogger(new Logger());
使用匿名类实现同样的功能:
<?php
$util->setLogger(new class {
public function log($msg) {
echo $msg;
}
});
重载
PHP 中的重载(overloading)与传统面向对象语言(如 Java、C++)中的重载概念不同。传统的重载允许同一个类中定义多个同名方法,但它们的参数个数或类型不同。而在 PHP 中,重载是通过动态创建类属性或方法实现的,主要用来处理未定义或不可见的类成员。
PHP 的重载关注动态属性或方法的处理,而不是方法的参数数量或类型。所有与重载相关的魔术方法都必须是 public 的。
PHP 提供的重载魔术方法
以下是与重载相关的魔术方法及其作用:
1. 属性重载
• __get($name)
当读取未定义或不可见的属性时触发。
• __set($name, $value)
当设置未定义或不可见的属性时触发。
• __isset($name)
当使用 isset() 或 empty() 检查未定义或不可见的属性时触发。
• __unset($name)
当调用 unset() 删除未定义或不可见的属性时触发。
2. 方法重载
• __call($name, $arguments)
当调用未定义或不可见的对象方法时触发。
• __callStatic($name, $arguments)
当调用未定义或不可见的静态方法时触发。
Final 关键字
在 PHP 中,final 关键字用于防止类或类成员(方法、属性)被继承或覆盖。当需要对类或方法进行保护,确保其行为不会被子类修改时,可以使用 final。
示例:
<?php
class BaseClass {
public function test() {
echo "BaseClass::test() called\n";
}
final public function moreTesting() {
echo "BaseClass::moreTesting() called\n";
}
}
class ChildClass extends BaseClass {
public function moreTesting() {
// 这段代码会产生错误,因为 moreTesting 是 final 方法。
echo "ChildClass::moreTesting() called\n";
}
}
?>
命名空间
从广义上来说,命名空间是一种组织和封装事物的方法,用于解决名称冲突的问题。这个概念在许多领域都可以见到,比如操作系统中的文件夹(目录)。
命名空间的概述
在操作系统中,两个文件名相同的文件(比如 foo.txt),可以分别存在于不同的目录中,例如 /home/greg/foo.txt 和 /home/other/foo.txt。但是,同一个目录中不能同时有两个文件名为 foo.txt 的文件。要访问具体的文件,就需要指定文件路径,例如 /home/greg/foo.txt。
同样的原理应用到 PHP 中,就形成了命名空间
的概念。
为什么需要命名空间
在 PHP 中,命名空间主要用来解决两个问题:
1. 名字冲突问题:当开发者定义的类、函数或常量名称,与 PHP 内置功能或第三方代码中的名称冲突时,就会报错或者出现意外行为。命名空间通过将代码归类组织,避免这些冲突。
2. 简化长名称:为了避免冲突,开发者有时会定义很长的标识符名称(如 MyApp_Utilities_StringHelper),但这些名称会降低代码的可读性。命名空间可以提供一种简短的引用方式来提高代码的易读性。
命名空间的特点
1. 大小写不敏感:命名空间名称是大小写不敏感的,例如 NamespaceA 和 namespacea 被视为同一个命名空间。
2. 保留名称:以 PHP 开头的命名空间(如 PHP\Classes)是保留给 PHP 内部使用的,用户代码中不应该使用这些名称。
3. 声明位置:命名空间必须在文件中所有其他代码之前声明(除了 declare 关键字之外)。
如何使用命名空间?
先使用 namespace 关键字声明命名空间。例如:
<?php
namespace MyProject\Sub\Level;
const CONNECT_OK = 1; // 定义一个常量
class Connection { /* ... */ } // 定义一个类
function connect() { /* ... */ } // 定义一个函数
?>
上面的 MyProject\Sub\Level 表示一个层次化的命名空间名称,与文件系统的目录结构类似。在命名空间中访问类、函数或常量时,可以使用以下三种方式:
• 非限定名称:不包含任何命名空间前缀,默认在当前命名空间中查找。
$obj = new foo(); // 等价于 \currentnamespace\foo()
• 限定名称:包含当前命名空间的前缀路径。
$obj = new subnamespace\foo(); // 定义在子命名空间中的类
• 完全限定名称:以 \ 开头的全局命名路径,不依赖当前命名空间。
$obj = new \MyProject\Sub\Level\foo();
__NAMESPACE__ 魔术常量
它的值是当前命名空间的名称(如果代码不在任何命名空间中,则返回空字符串)。
namespace MyNamespace;
echo __NAMESPACE__; // 输出 "MyNamespace"
namespace 关键字
可以简化在当前命名空间中访问其他元素的方式。
namespace MyNamespace;
$className = namespace\MyClass; // 等价于 MyNamespace\MyClass
枚举(Enum)
枚举(Enum,全称 enumeration)是 PHP 8.1 引入的一种功能,允许开发者定义一组离散且固定的值作为一种类型。这种类型对于明确和约束变量的取值范围非常有用,能够有效避免程序出现无效状态。
枚举使用 enum 关键字定义。例如:
<?php
enum Suit
{
case Hearts; // 表示 "红心"
case Diamonds; // 表示 "方块"
case Clubs; // 表示 "梅花"
case Spades; // 表示 "黑桃"
}
?>
以上代码定义了一个新的枚举类型 Suit,它包含四个有效值:Suit::Hearts、Suit::Diamonds、Suit::Clubs 和 Suit::Spades。
枚举值可以像对象一样赋值和比较:
<?php
$suit = Suit::Hearts;
if ($suit === Suit::Hearts) {
echo "This is Hearts!";
}
?>
异常
PHP 的异常模型类似于其他语言(如 Java 和 Python)。在 PHP 中,可以使用 throw 关键字抛出异常,并在 try 块中捕获异常。这样,异常的处理逻辑与正常逻辑被清晰地分隔,代码结构更加清晰且易于维护。
以下是一个简单的示例:
<?php
try {
// 可能抛出异常的代码
throw new Exception("An error occurred");
} catch (Exception $e) {
// 捕获异常并处理
echo "Caught exception: " . $e->getMessage();
} finally {
// 无论是否发生异常,这里的代码都会执行
echo "Finally block executed.";
}
?>
在 PHP 8.0 之前,throw 是一个语句,必须独占一行,例如:
throw new Exception("Error");
从 PHP 8.0 开始,throw 成为了一个表达式,这意味着它可以出现在任何表达式上下文中。例如:
<?php
// PHP 8.0 起支持
$value = $condition ? throw new Exception("Condition failed") : "Valid";
PHP 中除了异常,还有传统的错误处理方式(如 error_reporting 和 set_error_handler)。二者的区别在于:
• 错误(Error):通常指不可恢复的问题,例如内存不足或语法错误,通常会导致程序停止运行。
• 异常(Exception):用于处理可恢复的错误,开发者可以捕获并根据情况采取措施。
自 PHP 7 起,引入了一个新的错误处理机制,称为 Error 类。它将传统错误(如 E_ERROR)对象化,使得错误和异常可以统一处理。
纤程(Fiber)
纤程(Fiber)是 PHP 8.1 引入的一种轻量级并发工具。它允许你定义可以挂起和恢复执行的代码块,从而实现更高效的异步操作。相比传统的生成器(Generator),纤程拥有完整的调用栈,可以在任意函数调用中暂停和恢复,而不需要改变函数的定义方式。
在传统的同步编程中,如果某个操作(例如 I/O 操作)耗时较长,会阻塞整个程序。而通过纤程,PHP 提供了一种更优雅的方式,允许在执行到某一特定位置时暂停执行,并稍后恢复,这对于实现异步任务或协程模型非常有用。
示例(挂起和恢复纤程):
<?php
$fiber = new Fiber(function (): void {
// 挂起纤程并返回一个值
$value = Fiber::suspend('fiber');
echo "Value used to resume fiber: ", $value, PHP_EOL;
});
// 开始执行纤程,并获取 suspend 返回的值
$value = $fiber->start();
echo "Value from fiber suspending: ", $value, PHP_EOL;
// 恢复纤程,并传递一个值
$fiber->resume('test');
?>
输出结果:
Value from fiber suspending: fiber
Value used to resume fiber: test
生成器
生成器是一种简化迭代操作的工具,可以用较低的内存开销和复杂度,生成一系列值供 foreach 循环使用。与传统需要实现 Iterator 接口的方式相比,生成器的实现更加简洁,同时在处理大量数据时更加高效。
使用示例:
<?php
function xrange($start, $limit, $step = 1) {
if ($start <= $limit) { // 当起始值小于等于结束值时,按升序生成数据
if ($step <= 0) {
throw new LogicException('Step must be positive');
}
for ($i = $start; $i <= $limit; $i += $step) {
yield $i; // 使用 yield 生成每个值
}
} else { // 当起始值大于结束值时,按降序生成数据
if ($step >= 0) {
throw new LogicException('Step must be negative');
}
for ($i = $start; $i >= $limit; $i += $step) {
yield $i;
}
}
}
// 测试 range() 和 xrange()
echo 'Single digit odd numbers from range(): ';
foreach (range(1, 9, 2) as $number) {
echo "$number ";
}
echo "\n";
echo 'Single digit odd numbers from xrange(): ';
foreach (xrange(1, 9, 2) as $number) {
echo "$number ";
}
?>
输出结果:
Single digit odd numbers from range(): 1 3 5 7 9
Single digit odd numbers from xrange(): 1 3 5 7 9
生成器的执行流程
• 每次调用 yield 时,生成器会暂停执行,并将当前值返回给调用者。
• 当调用 foreach 迭代生成器时,会从上一次暂停的地方继续执行,直到遇到下一个 yield 或函数结束。
传统的 range(0, 1000000) 会生成一个包含百万个元素的数组,占用超过 100MB 内存。使用 xrange(0, 1000000) 的生成器方式,则只需占用不到 1KB 内存,按需生成每个值。