PHP中的参数和类型
类型决定了PHP脚本管理数据的方式,例如,使用字符串显示字符数据或使用字符串函数操作字符数据、在数学表达式中使用整数、在条件表达式中使用布尔类型等。这些类型被称为基本类型。
不过站在一个更高的角度看,一个类其实也定义了一种类型。因此ShopProduct对象不仅是object基本类型,同时也是ShopProduct类类型。一个类也是一种类型
在PHP中,方法和函数定义并不要求明确指定参数的类型。这既是一种便利,也是一种麻烦。“参数可以是任何类型”为代码带来了灵活性,但有时候这样灵活性也会引起歧义。
基本类型
在与方法参数和函数参数打交道时,检查参数类型格外重要。
下面使用一个错误示范:
假设需要从XML文件中提取配置的值,其中XML节点
resolve.xml文件内容
<settings>
<resolvedomains>false</resolvedomains>
</settings>应用程序需要将字符串fasle提取出来,并将其作为一个标识位传递给显示IP地址的方法outputAddresses()。outputAddresses()定义
<?php
class AddressManager
{
private $addresses = ['192.254.84.138','216.58.213.174'];
public function outputAddresses(bool $resolve)
{
foreach ($this->addresses as $address)
{
print $address;
if ($resolve)
{
print " (".gethostbyaddr($address).")";
}
print "\n";
}
}
}
$settings = simplexml_load_file(__DIR__ . "/resolve.xml");
$manager = new AddressManager();
$manager->outputAddresses((string)$settings->resolvedomains);然而,这段代码并不会如我们所期待的那样正常工作(因为是false,正常来说应该不会解析IP地址才对)。
在将字符串"false"传递给outputAddresses()方法时,我们误解了该方法对参数的隐式假设————该方法期待传入的是一个布尔类型的值(也就是true或false)。而字符串"false"其实会在测试参数类型时被转换为布尔值true。
请思考一下代码:
if("false")
{
//...
}实际上,等价于
if(true)
{
//....
}有多种方法可以解决这个问题。
一种方法是让outputAddresses()的限制更加宽松,即让它在识别到参数是字符串后,根据一些基本规则将参数转换为对应的布尔值。
public function outputAddresses($resolve)
{
if(is_string($resolve)
{
$resolve = (preg_match("/^(false|no|off)$/i",$resolve)) ? false : true;
}
//....
}不过我们应该避免采取这种设计方式。通常,为方法或者函数提供一个清晰、严格的接口,比模糊、宽松的接口更好。如果函数和方法的接口过于模糊、宽松,程序就可能混乱,继而产生bug。
我们还可以不修改outputAddresses(),只是加入一段注释,清晰地说明$resolve参数应当是一个布尔值。这种方法要求程序员必须阅读这段注释,否则在编程时就会犯错。
/**
* 输出 IP 地址列表
* 如果 $resolve 为 true 那么解析所有的 ip 地址
* @param $resolve boolean 是否解析 IP 地址?
*/
public function outputAddresses(bool $resolve)
{
//...
}如果编写客户端代码的程序员总是会勤勉地阅读注释,那么这也是一种合理的方法。
最后一种方法时让outputAddresses()严格地检查参数$resolve地数据类型。
public function outputAddresses($resolve)
{
// 判断参数类型是否为 布尔值
if(! is_bool($resolve))
{
exit('$resolve 参数不符合要求');
}
//....
}这种方法可以强制客户端代码确保$resolve的参数类型是正确的,否则程序就会报出警告。
总是,PHP是一门类型宽松的编程语言,所有类型很重要,我们无法指望编译器来阻止类型相关的bug。
对象类型
就像参数变量可能是基本类型,它也可能是对象类型。也就是说PHP的参数不仅能接受基本参数,也可以接受对象。所有对象也有类型。
下面的代码将ShopProduct对象作为参数
class ShopProductWriter
{
public function write($shopProduct)
{
$str = $shopProduct->title . ': '
. $shopProduct->getProducer()
. '(' . $shopProduct->price . ')';
print $str;
}
}测试一下ShopProductWriter这个类
$product1 = new ShopProduct('My Antonia','Willa','Cather',5.99);
$writer = new ShopProductWriter();
$writer->write($product1);结果
My Antonia: Willa Cather(5.99)ShopProductWriter类只有一个方法:write()。write()方法接收一个ShopProduct对象作为参数,并使用它的属性和方法来输出表示商品摘要的字符串。我通过变量名$shopProduct提示调用write()方法的客户端代码传递一个ShopProduct对象作为参数,但是我并没有强制它们这么做。这就表示可能收到一个出乎意料的对象或基本类型,而且直到调用$shopProduct的参数,我才能知道它的具体类型。而这个时候,已经有一段ShopProductWriter类的代码是基于“传递进来的就是参数ShopProduct对象”这个假设进行的了。
说简单点就是write()方法应当接受ShopProduct对象,但是可能接受了奇奇怪怪的参数了,可能是基本类型参数,也可能是其他对象。但是程序不会报错,因为PHP是类型宽松的语言。直到要执行write()方法时才意识到可能不是ShopProduct对象,此时代码已经运行了相当多了。这也就是bug产生的一种原因。
为了解决这个问题,PHP5引入了“类”类型声明(也被称为类型提示)。要想给方法参数加上类类型声明,只需要在想约束的方法参数前加上一个类名即可。
public function write(ShopProduct $shopProduct)
{
//....
}现在,write()只接受ShopProduct类型的对象作为$shopProduct参数。可以用下面的代码测试下
<?php
// php 服务器500错误解决
// 开启php.ini中的display_errors指令
ini_set('display_errors',1);
// 通过error_reporting()函数设置,输出所有级别的错误报告
error_reporting(E_ALL);
class ShopProductWriter
{
public function write(ShopProduct $shopProduct)
{
$str = $shopProduct->title . ': '
. $shopProduct->getProducer()
. '(' . $shopProduct->price . ')';
print $str;
}
}
class Worng
{
}
$shopProduct = new Worng();
$writer = new ShopProductWriter();
$writer->write($product1);结果
Fatal error: Uncaught TypeError: Argument 1 passed to ShopProductWriter::write() must be an instance of ShopProduct, null given, called in ....报错了一个致命错误,write() must be an instance of ShopProduct
这样做能够省去在使用参数前先检查参数类型的麻烦。而且可以让程序员一眼就看出调用write()方法的要求。
有了类类型声明,就可以对ShopProduct类添加一些限制了。
class ShopProduct
{
public $title;
public $producerMainName;
public $producerFirstName;
public $price = 0;
public function __construct(
string $title,
string $firstName,
string $mainName,
float $price
)
{
$this->title = $title;
$this->producerFirstName = $firstName;
$this->producerMainName = $mainName;
$this->price = $price;
}
//....
}这样修改构造方法后,就可以确保$title、$firstName和$mainName参数一定会是字符串类型,而$price一定会是浮点类型了。可以通过以错误的数据类型实例化ShopProduct来验证这点。
//实例化对象
$product1 = new ShopProduct('My Antonia','Willa','Cather',[]);结果
Fatal error: Uncaught TypeError: Argument 4 passed to ShopProduct::__construct() must be of the type float, array given, called in ....因为$price参数不是浮点数,所以报错了。
但是,如果可能的话,PHP会默认隐式地将参数转换为所需类型。例如将$price传入字符串的浮点数,则实例化不会失败。
$product1 = new ShopProduct('My Antonia','Willa','Cather','4.22');字符串'4.22'会被隐式地转换为浮点数4.22。
到目前为止,这个功能非常好用。但是回想一下AddressManager类遇到的问题。字符串'false'会被默认解析为布尔值true。默认情况下,即使我在AddressManager::outputAddresses()方法中声明参数为bool类型,情况依然发生。
public function outputAddresses(bool $resolve)
{
//...
}现在请思考传递字符串调用该方法的情况。
$manager = new AddressManager();
$manager->outputAddresses('false');因为这里会发生隐式转换,所以等同于传递一个布尔值true给outputAddresses方法。可以通过强类型(strict_types)声明来解决这个问题,不过这需要修改所有的代码文件。
$manager = new AddressManager();
declare(strict_types=1);
$manager->outputAddresses('false');现在程序就不会正常运行了。
strict_types声明只适用于调用方的代码,而不适用于函数或方法的实现代码。因此需要由客户端代码加强这种严格性。
有时,方法中需要可选参数。但是如果这么做了,又会限制它的类型。这时就可以通过提供参数默认值来满足这种需求。
class ConfReader
{
public function getValues(array $default = null)
{
$values = [];
// 做一些处理得到values
// 将values与默认值合并(永远是一个数组)
$values = array_merge($default,$values);
return $values;
}
}
评论已关闭