当类的功能是对一系列关联数据及其操作方法进行封装的复杂数据类型时,其中的数据就可以定义为类的“属性”,而对数据的操作则称为“方法”,虽然在类中定义方法与定义函数基本一样,但方法是面向对象编程概念中的一术语,所以,我们约定,如果是在类中定义的函数,我们称之为“方法”。
定义属性
如果属性的功能比较简单,可以直接设置或获取数据,我们就可以在类中定义公共的(public)变量作为属性来使用,如:
然后,我们使用如下的代码来使用这个属性:
在类的内部,我们同样可以使用这些变量,如$this->color。
定义方法
我们说过,类的方法定义与函数相似,只是在类中定义的方法需要访问性限定,如private、public(默认)和protected。如下面的代码。
然后,我们同样使用->访问对象的方法,如下面的代码。
使用getter和setter方法
前面我们使用了变量作为类的属性,但有些时候,类中的数据可能需要一些验证,如人类的年龄不能为负数,也不应该太大,如果单纯地使用变量,就无法有效的通过对象本身来进行验证;此时,我们可以考虑将这个数据变量定义为私有的(private),然后使用方法获取数据或对其赋值。
下面的代码,我们将使用Java风格的属性设置与访问方法,也就是使用getter和setter方法来获取或设置属性值。
我们首先在CHuman类中定义了私有变量age变量的值就可以了。而值得注意的是setAge()方法,它的功能是设置人的年龄数据,但是,我们知道,人的年龄是有限制的,如代码中的判断条件就是年龄必须是整数,必须大于等于0,而且小于等于150。
运行此代码,如果设置的年龄在0到150,则会显示相应的数值,如果设置的年龄不在指定范围内,则代码会抛出一个异常,提示信息为“年龄数据应该在0到150之间”。关于异常的处理,我们会在第6章讨论。
最后,再回顾一下Java风格的getter和setter方法:
- getter方法,一般命名为“get+属性名”的形式,此方法可以直接返回相应的属性变量的值,也可以根据需要按指定的形式返回相应的数据。
- setter方法,一般命名为“set+属性名”的形式,其参数则是带入的属性数据,在此方法中,可以对带入的数据进行检查,如果数据正确,则赋值给属性变量;如果数据不正确,则可以选择相应的处理,如抛出异常(程序会终止)或直接设置为默认值(程序会继续执行),具体采取哪种策略,还是需要根据实际情况来决定。
使用__get()和__set()方法
在类中定义__get()和__set()方法,可以对属性进行统一的管理,更厉害的是可以对属性进行动态管理;其工作原理是,在类的内部使用一个数组来保存属性名称和数据(键/值),然后,在__get()方法中自动返回指定名称的属性值;在__set()方法中,则将指定的属性值更新到属性数组中,如果数组中不存在指定的属性,则自动添加。
在CCard类中,我们定义了ParseError: KaTeX parse error: Expected group after '_' at position 23: …存放属性信息,然后,我们定义了_̲_get()方法和__set(…
在__get()方法中,我们通过key参数带入属性名称,然后,我们返回此键名称的数组成员值,即key]。
在__set()方法中,我们通过两个参数分别带入来设置属性的名称和数据,即键和值;然后,我们将此信息添加到数组中,即key] = $value。
在这里定义的CCard类中,我们初始化了三个属性,即name、age和phone。接下来,我们来使用CCard类,如下面的代码:
如果我们操作CCard类中没有定义的属性会出现什么情况呢?如下面的代码。
此时,我们可以发现,在使用CCard类中没有定义的属性organization时,似乎一切都很正常,无论是设置属性值,还是返回获取属性值,都和已定义属性的使用是一样的,这就是应用__get()和__set()方法的强大之处。不过,还有一些注意的地方:
- 我们可以看到,使用__get()和__set()方法可以动态管理属性,但是,如果类的应用要求非常严格,不允许使用不存在的成员,那么,完全动态的属性管理方式就是不合理的。不过,我们可以在__get()方法或__set()中使用array_key_exists()函数判断数组中的键(即属性名)是否存在,属性存在时进行相应的操作,不存在时则可能抛出异常,或者是其它的处理方法。
- 在__set()方法中,我们也可以根据不同的属性要求对带入的数据($value)进行检查,如果数据满足要求,则进行相应的操作,否则也可以抛出异常,或者是其它的处理方法,比如,设置为一个默认值,以保证代码能够顺利运行。
下面的示例,我们定义了更加严格的CCard类。
先看使用不存在的属性会出现什么情况,如下面的代码:
运行此代码,我们会在页面中看到类似如下图的错误信息。
返回不存在属性的值时,也会出现类似的信息,如下图。
再看属性值的检查,如下面的代码:
运行此代码会出现类似如下图的错误信息。
使用__call()方法
在类中,我们定义一个__call()方法,可以在对象调用类中没有定义的方法时进行相应的处理。请注意,__call()方法有两个参数:
- 第一个参数是调用方法的名称;
- 第二个参数是调用方法时所带入的参数,它是一个数组。
如下面的代码,我们演示了__call()方法的基本应用。
代码中,定义了CTest类,在其中只定义了__call()方法;然后,我们创建了CTest类的一个实例,即$obj对象。最后,我们调用了对象的sayHello()方法,而实际上,这个方法在CTest类中根据就没有定义,但运行的结果是,代码显示出了“Hello! Tom”信息,而这就是__call()方法在工作,它自动调用了在CTest类外部定义的sayHello()函数。
再来看一下__call()方法中的call_user_func_array()函数,它的功能是调用一个函数,其中,第一参数是调用方法的名称,从第二个参数是一个数组,用于带入调用函数的参数,其返回值将会是调用方法的返回值,如果调用失败则返回false值。
下面,我们再来看看单独使用call_user_func_array()函数的代码。
test()函数定义了两个参数,我们通过call_user_func_array()函数调用它时,就应该设置相同数量的参数,否则就会出现警告信息;如果确定没有相应的参数,我们可以带入NULL值作为参数,不过这样一来,在函数中就需要对参数进行严格的检查并做出相应的处理了。
此外,与call_user_func_array()功能类似的函数是call_user_func()函数,它第一个参数同样是调用函数的名称,区别在于,call_user_func()函数中,从第二个参数开始则是带入调用函数的参数,而这些参数可以由0个或多个。如下面的代码,其功能与上面的代码相同。
使用call_user_func()函数调用其它函数时,同样需要注意参数的数量问题,如果带入参数的数量不一致,就会产生警告信息。
此外,call_user_func_array()函数相对于call_user_func()函数,还有一个更多的选择,那就是在call_user_func_array()中调用函数的参数可以按引用传递。在后面的讨论中,我们还会看到相关的应用。
现在看上去,__call()方法的可能用途并不大,但是,如果在复杂的代码结构中,使用__call()方法则是实现“委托(delegate)”的好方法。如下面的代码。
首先,我们创建了En类和Cn类,它们都定义了同名的方法sayHello(),分别使用中文和英文显示您好(Hello)的信息。
然后是CLocale类,其中定义了一个
在CLocale类中定义的__call()方法中,我们使用了call_user_func_array()函数来调用locale对象的方法。
接下来,我们创建了obj->sayHello()方法将会显示“Hello”,因为其中的obj->locale,将其重新定义为Cn类的实例,然后再次调用$obj->sayHello()方法,此时会显示“您好”,因为此时调用的是Cn类中定义的sayHello()方法。
在使用__call()方法时,需要特别注意的是,如果调用的函数不存在,则会出现警告信息而无法继续运行,此时,我们应该对此情况做出一些相应的处理,如抛出异常,下面的代码就是对CLocale类中的__call()方法的修改。
代码中,我们使用method_exists()函数判断一个对象中是否存在指定的方法。如下面的代码,我们将调用一个不存在的方法test()。
运行此代码,会出现类似下面的提示。