【PHP进阶教程】第一课 类、属性、方法

由于是第一课,我们稍微上一点轻松的内容。以下内容可能会有些枯燥,但都是干货,希望大家读完以后能有自己的见解。

什么是类

对于学过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

发表评论