PHP 类和对象

预计阅读时间7 分钟 92 views

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 文件。这样做有一个小问题:每次你需要使用这些类时,都得在主脚本的最开始部分用includerequire语句把所有相关的文件引入进来。如果你有很多类,这个列表可能会变得非常长,而且难以管理。

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();

当我们创建MyClass1MyClass2的实例时,如果没有预先引入它们的文件,PHP会自动调用注册好的自动加载函数,尝试加载MyClass1.phpMyClass2.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 内存,按需生成每个值。

分享此文档

PHP 类和对象

或复制链接

本页目录