由于是第一课,我们稍微上一点轻松的内容。以下内容可能会有些枯燥,但都是干货,希望大家读完以后能有自己的见解。
什么是类
对于学过C++、JAVA的人来说,“类”这个概念应该是有的。类(Class),顾名思义就是一类事物的代称,例如动物是一个类,鸟类也是一个类,人类也是一个类。而说到这里,又要引出“对象”这个概念。对象(Object),就是某一个特定事物的代称,例如“莫乔多”是一个对象,因为指代的是一个事物,而不是一类事物。通常,我们以一个指向具体事物的名称、编号作为对象的标识。
类是抽象的,因为它不指代某一特定事物;对象是具象的,因为它指代某一特定事物。学过哲学的同学都知道,我们抽取出共同的、本质性的特征,而舍弃其非本质的特征的过程,叫做抽象。例如,我们知道人们都具有名字、性别、年龄等信息,将其抽离出来,人类的属性就是名字、性别、年龄等信息。但是在编程中,我们并不是一个自下而上的思维去开发,而是自上而下。我们不会从一个具体对象去抽离特定属性来做成类,而是直接考虑类有哪些属性和方法,再将类实例化对象去使用。这样说可能有人不能理解,但看了后面的例子,相信也会大概知道怎么做了。
总之,类就是一类对象特征的集合,这些对象都是由这个类实例化而来,且严格遵循这个类的规则运行。
上面提到了属性和方法,实际上,在类外,对应的是变量和函数,但是在类内,我们统一称为属性和方法,顺便一提,这是从JAVA继承过来的叫法。
以下的示例介绍了类、属性、方法的定义:
// 类使用Class开头定义,后接类名,类名后可接extends(继承)和implements(实现接口)
Class Student {
// 定义属性,PHP7.4之前无法指定其类型,但是可以通过IDE来智能识别
public $name;
private $sex;
protected $age;
/**
* 这是一个方法
*/
public function show()
{
echo "姓名:" . $name;
echo "性别:" . $sex;
echo "年龄:" . $age;
}
}
眼尖的同学可能发现了,这个例子里有三种不同的前缀:public、protected、private,学过C++、JAVA的同学应该清楚,这是修饰限定符,用来控制属性、方法是否对外开放,以下是它们权限的区别:
限定符 | 是否能在类内访问 | 是否能被继承 | 是否能在类外访问 |
public(默认) | √ | √ | √ |
protected | √ | √ | × |
private | √ | × | × |
在未指定限定符时,默认的限定符为public。
PHP的类跟JAVA的定义是差不多的,但有如下区别:
- PHP的方法无法重载,因为PHP支持默认参数,且PHP不对类型严格,若同时支持重载,则会导致翻译器无法判断调用的是哪个方法。但是重写是支持的。
- PHP支持魔术方法。所谓魔术方法,就是以一些手段来实现看似不可能实现的功能的方法。PHP的魔术方法以__开头(两个下划线),最常用的就是__get、__set、__call、__callStatic这几个,可以让某些不存在的或者保护的属性和方法以某种形式被外部调用。举个例子来说,我要通过一个类来动态存取数据,但是我不想事先声明属性,那么可以用__get和__set来代替属性的读写,类内只需要定义一个$data数组用于存放数据,用魔术方法来读$data。我的博客项目TarBlog就多出用到了这种方法,使得灵活性大大提高。
- PHP还有一个叫做Trait的特性,它跟类的定义很像,但仅为类服务,而不能独立存在(指实例化)。类通过use它来引入一些方法和属性,并能够覆盖(严格地说称之为重写)方法与属性。不过,通常它的用法主要是实现类似于多继承的效果,并且规避了多继承和Mixin机制的缺陷。
以上列出的仅是一些最基本的区别,实际上,学过JAVA的人都应该对PHP各类现代框架感到亲切——因为它们都基于类。(尽管PHP没有严格要求使用类作为应用程序的开发主体)
接下来,我们来仔细讲讲PHP中的类、属性、方法及相关内容。
属性
PHP的属性与JAVA中的属性定义也很相似,只不过PHP并不要求声明属性的类型,甚至在PHP7.4前,还无法在语法上指定属性的类型。因此,在默认情况下,属性的类型是随时有可能变动的。一般情况下,属性的默认值为null。属性的调用方式与JAVA相似,不过使用的符号不同:在类内,普通属性使用 $this->var 的形式进行调用,而静态(static)属性的调用方式则是 self::$var (有时也用 static::$var 代替,但它们有时候是有区别的,这个之后会讲到);在外部,普通属性使用 $object->var 的形式调用,静态属性用 MyClass::$var 进行调用。
通常情况下,我们不会直接在外部调用属性,一般都会将属性设置为private或protected。不过,有时为了快速进行数据交换(无需处理的情况下),也会将其设为public。这些都取决于你的规范,your code,your rule :)
PHP也允许在类内定义常量,且这些常量仅属于类,不会污染到全局。类内常量也有限定符,且默认为public。定义方式如下:
const ABC = 'abc'; // 等同于 public const ABC = 'abc';
方法
PHP的方法与JAVA中的方法类似,区别上面也说过,不允许重载。函数也是不允许重载的。但是PHP的函数/方法的返回值可以不限制类型,这使得程序更加灵活,也使得纠错机制更加复杂——除非事先有规定。许多方法的返回值为数组,使得数据结构变得更加复杂且不可预知,这就需要开发者自行设置一定的规范,否则会带来一定的麻烦。不过,相比JAVA只能返回特定类型返回值的特性,PHP在简单的数据结构上是有明显优势的。
PHP的方法就是函数加了个限定符,也可以不加,就默认是public。结构与JAVA相近,但JAVA定义返回值类型的地方被替换成function关键字;若想指定返回值(PHP7.1及以上),则在方法首部后面加上“: 返回值类型”,这样解释器就会去检测返回值类型。如下:
public function getValue() : string
{
return "abc";
}
如果返回值不为字符串,则会产生致命错误。
重写
与JAVA不同的是,PHP的属性是可以重写的,也就是没有所谓“隐藏字段”的概念。这是由于PHP创建对象无法指定变量类型,也就是一个对象它创建的时候必然是new后面的类型,这就导致“隐藏字段”是没有意义的。当然,若属性为静态,则不会被覆盖,静态属性是属于类的,跟对象无关。例如下面的代码:
class A {
public $abc = 'a';
public static $test = 'a';
public function test() {
echo $this->abc . "\n";
}
}
class B extends A {
public $abc = 'b';
public static $test = 'b';
}
$a = new A();
$b = new B();
$a->test();
$b->test();
echo A::$test . "\n";
echo B::$test . "\n";
输出的结果是 a b a b。这就意味着,子级的$abc覆盖掉了父级的$abc,父级所访问的$abc其实已经是子集的$abc了。而在JAVA中,结果会是a a a b。PHP的这个特性非常有用,后面会更加详细介绍。
方法的重写就基本与JAVA一致。值得一提的是,方法的重写必须要求参数与父级方法一致。
因private不存在继承关系,所以不会被重写。
继承与实现
所谓继承,跟字面意思一样,子类会获得父类所拥有的特征(除了被private标记的),而子类自己可以增加特性。
例如上一段代码,就展示了test方法是B从A中继承的,因此可以直接使用。同样的,属性的继承方式也是一样的。
PHP与JAVA一样,只支持单继承,也支持实现接口。但PHP有一个妙招可以实现“多继承”:
Trait
这是PHP独有的特性,它与类相像,但无法被继承和实例化。它虽然与多继承相像,但本质上没有破坏单继承的规则。它必须与类搭配使用。如何使用?很简单:
trait ezcReflectionReturnInfo {
function getReturnType() { /*1*/ }
function getReturnDescription() { /*2*/ }
}
class ezcReflectionMethod extends ReflectionMethod {
use ezcReflectionReturnInfo;
/* ... */
}
class ezcReflectionFunction extends ReflectionFunction {
use ezcReflectionReturnInfo;
/* ... */
}
这是PHP官方的一个例子,可以看到,在trait中定义了两个方法。在类内使用 use 关键字即可将这两个方法加入到类中。同样的,在trait内也可以使用 use 关键字来“合并”。若要加入多个Trait,可以用逗号隔开它们:
trait Hello {
public function sayHello() {
echo 'Hello ';
}
}
trait World {
public function sayWorld() {
echo 'World';
}
}
class MyHelloWorld {
use Hello, World;
public function sayExclamationMark() {
echo '!';
}
}
$o = new MyHelloWorld();
$o->sayHello();
$o->sayWorld();
$o->sayExclamationMark();
trait还涉及到冲突的问题。一般情况下,不会发生冲突,但有必要了解一下:https://www.php.net/manual/zh/language.oop5.traits.php#language.oop5.traits.conflict
抽象类
在JAVA中,也有抽象类的概念。抽象类无法被直接实例化,只有抽象方法被子类实现后,才能通过子类实例化。若子类不实现抽象方法,则会报错。代码示例:
abstract class AbstractClass
{
// 强制要求子类定义这些方法
abstract protected function getValue();
abstract protected function prefixValue($prefix);
// 普通方法(非抽象方法)
public function printOut() {
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();
echo $class1->prefixValue('FOO_') ."\n";
$class2 = new ConcreteClass2;
$class2->printOut();
echo $class2->prefixValue('FOO_') ."\n";
关于类的内容,基本上就这么多。不过,我这里再补充几点知识。
什么是接口
相信大家对接口的概念还不是很熟悉,我这里做一个简单的介绍。接口,是一个只定义不实现的跟类很像的东西。它不能定义属性,只能定义方法。例如:
interface A {
public function a();
public function b();
}
目前来看,还很抽象,但是,它就是抽象的。上面我们提到了抽象的概念,这就是一种抽象。也就是说,接口只给了个外壳,内容物需要另外去“填充”。那怎么填充呢?看下面:
class B implements A {
public function a() {
echo "I'm a method";
}
public function b() {
echo "I'm b method";
}
}
我觉得你看了以后,会说:“就这?”
没错,就这。这就是接口和其实现。接口可以在多个类上实现,一个类也可以实现多个接口。
接口有什么用?为什么要定义接口?
承接上面的话题,我们来谈谈接口的用处。接口实际上是一种规范,规定了动作和结果,但不关心中间的实现过程。说白了,就是一个黑匣子,你只关心某个功能是否可用,而不关心这个功能是怎么实现的。而实现则反过来,我已经知道要产生动作和结果,但是我得去实现它,所以我编写一系列代码去完成这个接口。事实上,这种关系就像是产品经理和开发的关系。有了接口这一概念,开发产品的思路就更加清晰,而不会说我不知道要开发什么。
至于为什么要定义接口,上面也说了,接口是一种规范,我们定义接口的目的,就是想让实现接口的开发人员去遵守规范,让他们老老实实去按规范实现功能,而不是自己另外定一套规则,到时候一对接,两边都芜湖起飞,互相推锅。没错,接口非常利于模块化编程,非常利于分工,而且最后对接也会非常方便。虽然PHP不严格定义返回值,但至少最后可以调用到设计好的方法,问题会好解决许多。这就是定义接口的原因,只为了方便。当然,如果并不是对外、对团队开放的内容,确实没有必要定义接口,这个思路要自己理清楚来。
总而言之,接口,在JAVA和PHP中都十分有用,这是拥有一定开发经验的人才能感觉出来的。
PHP能做到像JAVA那样完全面向对象编程吗
很遗憾,PHP的设计最初是偏向于面向过程的,面向对象的特性是后来的版本加上的。不过,我们可以设计出一套框架,将面向过程的内容“屏蔽”,剩下的应用层则全部为面向对象的结构。现在许多框架,就是用这种思路实现了类JAVA的开发体验。我们即将在后面学习的MVC架构,也会用到面向对象编程的思想。如果对面向对象编程不了解,也没关系,跟着本教程的思路走,你也会逐渐理解的。
这就是本课的全部内容了,下一课我们将开始学习MySQL数据库的简单使用,包括MySQL的连接(通过独立客户端)、增删查改、SQL语句编写等。如果还没有搭建好MySQL,可以参考基础教程第一课:https://moqiaoduo.cn/223.html