PHP中的访问修饰符有哪些?各有什么作用?

在现代面向对象编程(Object-Oriented Programming, OOP)中,封装(Encapsulation)是其核心支柱之一。封装旨在将对象的内部状态(属性)和行为(方法)捆绑在一起,并对外部世界隐藏其复杂的实现细节。为了实现这一目标,PHP提供了访问修饰符(Access Modifiers)作为关键工具。这些修饰符定义了类的成员(属性、方法和常量)在不同上下文中的可见性和可访问性。

本报告旨在深入、全面地研究PHP中的访问修饰符,不仅涵盖了经典的publicprotectedprivate,还探讨了它们在类继承、方法覆盖等复杂场景下的行为规则。此外,报告将结合PHP新版本(如PHP 8.x)引入的readonly修饰符及相关特性,分析其对访问控制机制的增强和影响。报告还将阐述这些修饰符在接口(Interface)、特质(Trait)、抽象类(Abstract Class)和类常量(Class Constant)等特殊结构中的应用规则,以期为PHP开发者提供一份详尽的参考指南。


2. 核心访问修饰符:publicprotectedprivate

PHP主要提供了三种核心的访问修饰符,它们共同构成了访问控制的基础。

2.1 public (公共的)

public修饰符提供了最宽松的访问级别。

  • 作用范围:被声明为public的类成员(属性或方法)可以在程序的任何地方被访问。这包括在定义它的类内部、继承它的子类中,以及在类的外部通过对象实例进行调用。简而言之,它是完全公开的 。
  • 默认修饰符:在PHP中,如果一个类成员没有被显式指定任何访问修饰符,它将被默认为public 。
  • 使用场景public成员通常构成了类的“公共API(应用程序编程接口)”。它们是类希望与外部代码进行交互的入口点。典型的使用场景包括:
    • 类的构造函数(__construct),通常需要公开以便实例化。
    • 作为公共接口的方法,例如一个User类的getName()login()方法。
    • 需要被外部直接读取或设置的配置类属性。
  • 示例代码
class Car {
    public string $brand; // 公共属性

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

    public function startEngine(): void { // 公共方法
        echo "{$this->brand} engine started.";
    }
}

$myCar = new Car("Toyota");
echo $myCar->brand; // 从外部访问公共属性
$myCar->startEngine(); // 从外部调用公共方法
2.2 protected (受保护的)

protected修饰符提供了介于publicprivate之间的访问级别,主要用于处理继承关系。

  • 作用范围:被声明为protected的成员只能在定义它的类本身及其所有子类(派生类)中被访问。它不能被类的外部代码直接访问 。
  • 使用场景protected非常适合用于定义基类(父类)中那些不希望对外界公开,但又需要被子类继承、使用或重写的内部实现。这为构建可扩展的类库和框架提供了灵活性,同时保持了良好的封装性。典型的使用场景包括:
    • 基类中供子类重写的模板方法。
    • 在继承体系内共享的辅助函数或内部状态属性。
  • 示例代码
abstract class Shape {
    protected float $area; // 受保护的属性,子类可以访问

    // 受保护的方法,供子类使用或重写
    protected function calculateArea(): void {
        // 通用计算逻辑
    }

    abstract public function getDimensions(): string;
}

class Circle extends Shape {
    private float $radius;

    public function __construct(float $radius) {
        $this->radius = $radius;
        $this->calculateArea(); // 在子类内部可以访问父类的protected方法
    }

    // 重写父类的protected方法
    protected function calculateArea(): void {
        $this->area = pi() * $this->radius * $this->radius; // 访问并修改父类的protected属性
    }

    public function getDimensions(): string {
        return "Radius: {$this->radius}, Area: {$this->area}";
    }
}

$circle = new Circle(10);
echo $circle->getDimensions(); // 正确
// echo $circle->area; // 致命错误:无法从外部访问受保护的属性
// $circle->calculateArea(); // 致命错误:无法从外部调用受保护的方法
2.3 private (私有的)

private修饰符提供了最严格的访问级别,是实现强封装的核心。

  • 作用范围:被声明为private的成员仅仅能在定义它的那个类(The defining class)的内部被访问。即使是继承该类的子类,也无法访问父类的private成员 。
  • 使用场景private成员用于封装一个类的内部实现细节。这些细节对于类的正常运作至关重要,但不应该被外部代码或其他类所了解或依赖。将成员设为private可以防止外部代码无意中破坏类的内部状态,从而提高代码的健壮性和可维护性。如果确实需要与外界交互,通常会通过public的getter和setter方法作为受控的访问渠道。
  • 示例代码
class BankAccount {
    private float $balance; // 私有属性,封装账户余额

    public function __construct(float $initialDeposit) {
        if ($initialDeposit < 0) {
            $this->balance = 0;
        } else {
            $this->balance = $initialDeposit;
        }
    }

    public function deposit(float $amount): void {
        if ($amount > 0) {
            $this->balance += $amount;
            $this->logTransaction("Deposited: " . $amount); // 在类内部调用私有方法
        }
    }

    public function getBalance(): float { // 公共的getter方法
        return $this->balance;
    }

    private function logTransaction(string $message): void { // 私有方法,用于内部记录
        // 内部日志记录逻辑...
        echo "Log: {$message}\n";
    }
}

$account = new BankAccount(100);
$account->deposit(50);
echo "Current Balance: " . $account->getBalance(); // 通过公共方法获取余额
// echo $account->balance; // 致命错误:无法从外部访问私有属性
// $account->logTransaction("Test"); // 致命错误:无法从外部调用私有方法

3. 访问修饰符在继承与方法覆盖中的行为

访问修饰符在类继承体系中的行为遵循一套明确的规则,这对于维护OOP中的“里氏替换原则”至关重要。

3.1 继承中的可见性规则
  • 父类的publicprotected成员可以被子类继承。子类可以直接像访问自己的成员一样访问这些继承来的成员 。
  • 父类的private成员不会被子类继承。从子类的角度看,父类的私有成员是不存在的。如果子类定义了一个与父类私有成员同名的成员,那么它是一个全新的、与父类无关的成员 。
3.2 方法覆盖(Overriding)的可见性规则

当子类重写父类的方法时,其访问权限必须等于或比父类更宽松。这一规则确保了子类的对象可以无缝替换父类的对象,而不会破坏原有的接口契约。

  • 如果父类方法是public,那么子类中重写的方法也必须是public 。
  • 如果父类方法是protected,那么子类中重写的方法可以是protectedpublic,但不能是private 。
  • 父类的private方法由于不能被继承,因此也谈不上被“覆盖”。子类可以定义一个同名的方法,但这只是一个新方法,并非覆盖。

此外,使用final关键字修饰的方法不能被任何子类覆盖 。

示例代码

class ParentClass {
    public function publicMethod() {}
    protected function protectedMethod() {}
    private function privateMethod() {}
}

class ChildClass extends ParentClass {
    // 正确:保持public
    public function publicMethod() {}

    // 正确:将protected放宽为public
    public function protectedMethod() {} 

    // 这不是覆盖,只是一个与父类无关的新方法
    private function privateMethod() {} 
}

class AnotherChildClass extends ParentClass {
    // 错误:将public收紧为protected
    // protected function publicMethod() {} // Fatal error: Access level must be public (as in class ParentClass)

    // 正确:保持protected
    protected function protectedMethod() {}
}

4. PHP新版本中的相关特性与修饰符

随着PHP语言的不断演进,访问控制相关的机制也得到了增强。

4.1 PHP 8.0:构造函数属性提升

PHP 8.0引入了构造函数属性提升(Constructor Property Promotion)语法糖,极大地简化了在构造函数中声明并初始化属性的代码。访问修饰符是这一特性的核心组成部分。开发者可以在构造函数的参数前直接加上publicprotectedprivate,PHP会自动将其提升为同名、同可见性的类属性,并完成赋值操作。

示例代码

// PHP 8.0 之前
class User_Old {
    private string $username;

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

// PHP 8.0 及之后使用属性提升
class User_New {
    public function __construct(private string $username) {
        // 无需额外代码,属性已声明并初始化
    }
}
4.2 PHP 8.1:readonly 修饰符

PHP 8.1引入了readonly修饰符,用于创建不可变(Immutable)属性,为数据封装和对象状态管理提供了更强的保障。

  • 定义与作用readonly修饰的属性一旦在类作用域内被初始化后,就不能再次被修改。尝试修改会抛出Error异常 。
  • 使用规则
    • readonly只能用于类型化(Typed)属性 。
    • readonly属性不能有默认值,必须在声明时或在构造函数中初始化 。
    • 初始化操作必须在声明该属性的类内部完成 。
  • 与访问修饰符的结合readonly可以与publicprotectedprivate结合使用,以同时控制属性的可见性和可变性 。

示例代码

class Transaction {
    // 结合使用访问修饰符和readonly
    public readonly string $transactionId;
    protected readonly DateTimeImmutable $createdAt;
    
    public function __construct(
        private readonly float $amount // 构造函数属性提升与readonly结合
    ) {
        $this->transactionId = uniqid();
        $this->createdAt = new DateTimeImmutable();
    }

    public function getDetails(): string {
        // 在类内部可以读取readonly属性
        return "ID: {$this->transactionId}, Amount: {$this->amount}, Time: {$this->createdAt->format('Y-m-d H:i:s')}";
    }
}

$tx = new Transaction(150.75);
echo $tx->getDetails();
// $tx->transactionId = 'new_id'; // Fatal error: Cannot modify readonly property Transaction::$transactionId
.3 PHP 8.2:动态属性的弃用

虽然不是直接的修饰符变更,但PHP 8.2开始弃用动态属性(即在运行时为一个对象创建未事先声明的属性)。这一变化间接强化了访问修饰符的重要性,因为它鼓励开发者在类定义中明确声明所有属性及其可见性,从而使代码结构更清晰、更易于静态分析和维护。


5. 访问修饰符在特殊结构中的应用

除了常规的类,访问修饰符在PHP其他OOP结构中也有特定的应用规则。

  • 类常量 (Class Constants) :自PHP 7.1.0起,类常量也可以使用publicprotectedprivate进行修饰,其行为规则与属性类似。在此之前,所有类常量都默认为public

  • 接口 (Interfaces)

    • 接口中定义的所有方法都必须是public,且不能使用protectedprivate。这是因为接口的目的是定义一个公共契约,供不同的类去实现。
    • 接口中可以定义常量,这些常量隐式地为public
  • 特质 (Traits)

    • 特质是一种代码复用机制,其内部的属性和方法可以使用publicprotectedprivate所有三种访问修饰符。
    • 当一个类使用(use)一个特质时,特质的成员会被“复制”到类中,并保持其原有的可见性。类还可以使用as关键字来修改引入成员的可见性,例如将一个protected方法在当前类中变为public
  • 抽象类 (Abstract Classes)

    • 抽象类中的普通成员(非抽象属性和方法)遵循标准的访问修饰符规则。
    • 抽象方法(只有声明没有实现的方法)可以是publicprotected,但不能是private。因为private方法不能被子类访问,也就无法被子类实现,这与抽象方法的目的相悖。

6. 总结与最佳实践

PHP的访问修饰符是实现强大而健壮的面向对象设计的基石。通过合理运用publicprotectedprivate以及新引入的readonly,开发者可以精确控制代码的封装性、可扩展性和安全性。

核心最佳实践建议

  1. 最小权限原则:默认将所有成员设为private。只有当需要被子类访问时,才将其放宽为protected。只有当需要构成类的公共API时,才将其设为public
  2. 明确公共APIpublic成员是类对外的承诺。一旦发布,应谨慎修改,以免破坏依赖此接口的外部代码。API设计应力求简洁、稳定。
  3. 善用protected设计继承protected成员是为子类设计的“内部API”。通过它们,可以构建出灵活且可扩展的继承体系。
  4. 拥抱不可变性:对于那些一旦创建就不应改变值的属性,积极使用PHP 8.1的readonly修饰符。这能有效防止意外的状态变更,减少bug,使代码逻辑更清晰、可预测。
  5. 显式声明:遵循PHP 8.2及以后的趋势,避免使用动态属性。显式声明所有属性及其访问修饰符,可以提升代码质量和可维护性。

通过遵循这些原则,开发者可以构建出结构清晰、高内聚、低耦合且易于维护和扩展的PHP应用程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

破碎的天堂鸟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值