💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
## 什么是继承? 这个继承词已经被传了这么多,我最好的赌注是你已经在某种程度上弄清楚了它的意思。但是,让我们来看看OOP中继承的正式定义。继承是一种允许类继承另一个类的属性和方法的机制。继承另一个类的属性和方法的类称为子类,继承的类称为父类。 继承允许我们在现有类的基础上构建类,允许我们重用代码并减少我们编写的代码量。正如我在本系列之前所指出的,OOP的灵感来自于人类如何思考和与周围环境互动。继承也是基于这个模型。我们周围的很多物体都是其他物体的特殊化。例如,汽车是交通工具的特化,狗是动物的特化,等等,交通工具和动物有这么多,但它们都有一些共同的属性和方法。单独定义每个类中的属性和方法完全是浪费时间。我们可以做的是将所有公共属性和方法提取到一个单独的类中,然后创建其他单独的类来扩展该基类。 ## PHP OOP中的继承 在PHP OOP中实现继承非常简单--使用`extends`关键字。我们来看一个例子: ```php class Animal { /** * 构造函数,用于对象初始化 * * @param string $name 设置对象的名称属性 * @param int $age 设置对象的年龄属性 * @param string $color 设置对象的颜色属性 */ public function __construct( public string $name, public int $age, public string $color ) { } /** * 本函数通过输出字符串模拟对象进餐的动作,使用的名称是对象初始化时提供的名称 * 选择直接使用echo进行输出而不是返回值,是因为这里强调的是动作本身而非返回结果 */ public function eat(): void { echo $this->name . " is eating"; } /** * 该方法直接在对象的上下文中使用,用于显示对象正在睡觉的信息 * 它主要用于演示或调试目的,提供了一种简单的方法来标识对象状态 */ public function sleep(): void { echo $this->name . " is sleeping"; } } ``` 这个`Animal`类定义了所有动物通用的属性和方法。现在,让我们创建一个继承自`Animal`类的`Dog`和`Cat`类。 ``` class Dog extends Animal { public function bark() : void { echo $this->name . " is barking"; } } class Cat extends Animal { public function meow() : void { echo $this->name . " is meowing"; } } $dog = new Dog("Rex", 5, "Brown"); $dog->eat(); // Rex is eating $dog->bark(); // Rex is barking $dog->sleep(); // Rex is sleeping $cat = new Cat("Tom", 3, "White"); $cat->eat(); // Tom is eating $cat->meow(); // Tom is meowing $cat->sleep(); // Tom is sleeping ``` Dog和Cat类继承自Animal类,以及它所附带的所有内容(属性和方法)。它们也有自己独特的属性和方法。`狗叫()`,猫`叫()`。 从示例中可以清楚地看到,在PHP中,多个类可以从一个类继承。这就是所谓的*多级继承*。一个子类也可以有它自己子类,所以我们基本上可以有一个类的家谱。这就是所谓的*分层继承*。Eg.`Animal`类是`Dog`和`Cat`类的父类,`Dog`类是`GermanShepherd`和`Bulldog`类的父类。 ``` class GermanShepherd extends Dog { public function __construct( public string $name, public int $age, public string $color ) { } public function guard() : void{ echo $this->name . " is guarding"; } } class Bulldog extends Dog { public function __construct( public string $name, public int $age, public string $color ) { } public function guard() : void{ echo $this->name . " is guarding"; } } ``` ## 什么是多态性? 多态性与继承密切相关。它允许将不同类的对象视为同一类的对象。它允许我们以不同的方式执行一个动作。它允许在不需要知道对象的确切类型的情况下调用方法。多态性是一个希腊词,意思是“多种形式”。它是OOP的一个强大功能,允许我们编写灵活和可重用的代码。 让我们用一个类比来说明这一点。假设我们有一个`Vehicle`类,它有两个方法-`drive()`和`startEngine()`。假设我们有三个类-`Car`、`Motorcycle`和`Bicycle`,它们继承自`Vehicle`类,因此它是方法。 ``` class Vehicle { public function startEngine() : void{ echo "Starting the engine of the vehicle."; } public function move() : void{ echo "Driving a vehicle"; } } class Car extends Vehicle { public function startEngine() : void{ echo "Starting the engine of the car with a key."; } public function move(): void { echo "Driving a car with 4 wheels"; } } class Motorcycle extends Vehicle { public function startEngine() : void { echo "Starting the engine of the motorcycle with a kick."; } public function move() : void { echo "Driving a motorcycle with 2 wheels"; } } class Bicycle extends Vehicle { public function startEngine() : void { echo "Bicycles don't have engines. Just pedal."; } public function move() : void { echo "Driving a bicycle with 2 wheels"; } } $car = new Car(); $car->startEngine(); // Starting the engine of the car with a key. $car->move(); // Driving a car with 4 wheels $motorcycle = new Motorcycle(); $motorcycle->startEngine(); // Starting the engine of the motorcycle with a kick. $motorcycle->move(); // Driving a motorcycle with 2 wheels $bicycle = new Bicycle(); $bicycle->startEngine(); // Bicycles don't have engines. Just pedal. $bicycle->move(); // Driving a bicycle with 2 wheels ``` 现在这里没有什么新的东西可看。从那以后我们就一直在继承遗产。现在让我们看看多态性是如何发挥作用的。 ``` class Player { private Vehicle $vehicle; public function setVehicle(Vehicle $vehicle) { $this->vehicle = $vehicle; } public function drive() { $this->vehicle->startEngine(); $this->vehicle->move(); } } $car = new Car(); $motorcycle = new Motorcycle(); $bicycle = new Bicycle(); $player = new Player(); $player->setVehicle($car); $player->drive(); // Starting the engine of the car with a key. Driving a car with 4 wheels $player->setVehicle($motorcycle); $player->drive(); // Starting the engine of the motorcycle with a kick. Driving a motorcycle with 2 wheels ``` 下面是发生的情况:`Player`类的`$vehicle`属性和`setVehicle()`方法需要一`个Vehicle`类型。但是,我们能够传入`汽车`和`摩托车`对象。当我们调用`$vehicle`的`startEngine()`和`move()`方法时,额外的魔力就来了。`Player`类不需要知道它所处理的对象的确切类型。它只是调用方法,然后调用适当的方法。这就是多态性的作用。 ## 方法覆盖 使多态性成为可能的一件事是方法重写。方法重写是子类重写其父类的方法的能力。在前面的例子中,`Car`、`Motorcycle`和`Bicycle`类都覆盖`了Vehicle`类的`startEngine()`和`move()`方法。在某些语言中,默认情况下不会重写方法。你必须显式地指定你想要重写一个方法。在PHP中,方法被默认覆盖。 另外,有些语言支持方法重载。方法重载是指拥有多个同名但参数不同的方法的能力。PHP不支持方法重载。 这里有一个方法重载的例子,如果我们在PHP中有它的话,它会是什么样子。 ``` class Vehicle { public function startEngine() { echo "Starting the engine of the vehicle."; } public function move() { echo "Driving a vehicle"; } } class Car extends Vehicle { public function startEngine() { echo "Starting the engine of the car with a key."; } public function startEngine(string $key) { echo "Starting the engine of the car with a $key."; } public function move() { echo "Driving a car with 4 wheels"; } } $car = new Car(); $car->startEngine(); // Starting the engine of the car with a key. $car->startEngine('blue key'); // Starting the engine of the car with a blue key. ``` > 注:上面的代码会抛出一个错误,因为PHP不支持方法重载。这只是痴心妄想! ### 协变和逆变 协变性和逆变性听起来像是很大的术语,但相信我,它们并不像看起来那么复杂。 协方差是最容易理解的。它允许一个方法的返回类型在子类中是一个比父类中相同方法返回的类型更具体的类型。这是一个很大的问题,让我们来看看一个例子。 ``` class Vehicle { public static function make(): Vehicle { return new Vehicle(); } } class Car extends Vehicle { public static function make(): Car { return new Car(); } } ``` `Vehicle`类的`make()`方法返回一个`Vehicle`类型。但是在`Car`类中,我们将返回类型专门化为`Car`。这就是协方差的作用。你可以阅读[Liscov替换原理](https://en.wikipedia.org/wiki/Liskov_substitution_principle)来理解为什么这是可能的。这意味着子类的返回类型不能不具体。 另一方面,矛盾性稍微复杂一点。它使用方法参数而不是返回类型。它允许一个方法的参数类型在子类中是一个比父类中相同参数的类型更不特定的类型。使用我们`的Vehicle`和`Car`类,如果一个方法需要`Vehicle`,自然地,我们可以传入一`个Car`对象。没什么新鲜事我们以前见过也做过。但是,如果方法需要一`个Car`对象,我们可以传入一个`Vehicle`对象吗?这就是逆变出现的地方。让我们看一个例子。 ``` class Driver { public function drive(Car $car) { echo "Driving a car"; } } $driver = new Driver(); $driver->drive(new Car()); // Driving a car ``` 现在,如果你试图传入一个`Vehicle`对象,你会得到一个错误。如果我们有一个从`Driver`继承的类,那么Contraversion就开始起作用了。 ``` class UberDriver extends Driver { public function drive(Vehicle $vehicle) { echo "Driving a vehicle"; } } $uberDriver = new UberDriver(); $uberDriver->drive(new Vehicle()); // Driving a vehicle ``` 即使父类需要一`个Car`或它的任何子类,我们也可以覆盖该方法,使其需要一个`Vehicle`对象。这就是作用中的逆变。一开始可能会有点困惑,但你会掌握窍门的。 ### 操作符 在前面的`UberDriver`示例中,如果我们想检查传入的对象是否是`Car`对象,该怎么办?我们不能使用`is_a()`函数,因为它将返回`false`,因为传入的对象是`Vehicle`对象。这就是`instanceof`操作符的用武之地。它允许我们检查一个对象是类的实例还是它的子类。所以我们可以这样做: ``` class UberDriver extends Driver { public function drive(Vehicle $vehicle) { if($vehicle instanceof Car) { echo "Driving a car"; } else { echo "Driving a vehicle"; } } } $uberDriver = new UberDriver(); $uberDriver->drive(new Vehicle()); // Driving a vehicle $uberDriver->drive(new Car()); // Driving a car ``` `instanceof`检查一个对象是类的实例还是它的子类。它不检查一个对象是否是父类的实例。所以如果我们做这样的事情: ``` $car = new Car(); $car instanceof Vehicle; ``` 它应该给我们真实的,但类似于: ``` $vehicle = new Vehicle(); $vehicle instanceof Car; ``` ## 抽象类和方法 让我们在车辆类比的上下文中反映这一点。在真实的意义上,我们永远不会有一辆车。我们将有一辆汽车,摩托车,自行车等,所以它没有意义有一个`车辆`类。这就是**抽象**类和方法概念的由来。 抽象类是不能自己实例化的类(即不能从该类创建对象)。它们是用来继承的。它们作为其他类的蓝图,类似于常规类,但有一个关键的区别:它们可以包含抽象方法,这些方法没有实现。抽象方法是声明但未实现的方法。它们应该在孩子的课堂上实施。抽象类也可以包含带有实现的常规方法。因此,让我们重构代码以使用抽象类。 ``` abstract class Vehicle { abstract public function start(): void; abstract public function stop(): void; public function refuel(): void { // Refueling process for vehicles } } class Car extends Vehicle { public function start(): void { echo "Starting the engine of the car with a key."; } public function stop(): void { echo "Stopping the engine of the car."; } } class Motorcycle extends Vehicle { public function start(): void { echo "Starting the engine of the motorcycle with a kick."; } public function stop(): void { echo "Stopping the engine of the motorcycle."; } } class Bicycle extends Vehicle { public function start(): void { echo "Bicycles don't have engines. Just pedal."; } public function stop(): void { echo "Stopping the bicycle."; } } ``` 在这个抽象类中,我们将`start()`和`stop()`定义为抽象方法。这些方法没有在抽象类中实现,但提供了一个公共接口,任何从`Vehicle`继承的类都必须实现该接口。从`Vehicle`继承的具体类必须实现`start()`和`stop()`方法(如果不实现,IDE将抛出错误)。`refuel()`方法是一个有实现的常规方法。如果需要的话,它可以在子类中被重写。 这并不意味着我们不能将`Vehicle`作为返回类型或方法参数。我们仍然可以拥有它。只是不能实例化它。所以我们可以这样做: ``` class Driver { public function drive(Vehicle $vehicle) { $vehicle->start(); $vehicle->move(); $vehicle->stop(); } } ``` ### 为什么要使用抽象类和方法? 是的,在某种程度上,使类抽象似乎是多余的。为什么不让他们正常?我们使用抽象类和方法有几个原因。抽象类和方法允许您创建其他类遵循的蓝图,在代码库中强制执行特定结构。`abstract`关键字还允许您防止类被实例化,从而保持严格、紧密和有组织-没有机会在这方面出现错误。 ## 最终类和方法 说到保持严格,`最后`一个关键字是另一种方式。`final`关键字防止类被继承,防止方法被重写。现在有很多关于是否使用`final`关键字的争论。有些人说使用它是一个很好的做法,而另一些人则说不是。我们有一个叫做`unfinalize的`包,用来从包中的类中删除`final`(实际上它有很多粉丝)。我不想偏袒任何一方。我只告诉你怎么用,然后你自己决定。 ``` final class Car extends Vehicle { public function start(): void { echo "Starting the engine of the car with a key."; } public function stop(): void { echo "Stopping the engine of the car."; } } ``` `汽车`类现在是最后一个类。它不能被继承。如果你尝试这样做,你会得到一个错误。方法也是如此。如果你试图覆盖一个final方法,你会得到一个错误。