类型决定了PHP脚本管理数据的方式,例如,使用字符串显示字符数据或使用字符串函数操作字符数据、在数学表达式中使用整数、在条件表达式中使用布尔类型等。这些类型被称为基本类型。

不过站在一个更高的角度看,一个类其实也定义了一种类型。因此ShopProduct对象不仅是object基本类型,同时也是ShopProduct类类型。一个类也是一种类型

在PHP中,方法和函数定义并不要求明确指定参数的类型。这既是一种便利,也是一种麻烦。“参数可以是任何类型”为代码带来了灵活性,但有时候这样灵活性也会引起歧义。

基本类型

在与方法参数和函数参数打交道时,检查参数类型格外重要。

下面使用一个错误示范:

假设需要从XML文件中提取配置的值,其中XML节点指定应当解析IP地址还是域名。

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()方法时,我们误解了该方法对参数的隐式假设————该方法期待传入的是一个布尔类型的值(也就是truefalse)。而字符串"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');

因为这里会发生隐式转换,所以等同于传递一个布尔值trueoutputAddresses方法。可以通过强类型(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;
    }
}

标签: none

评论已关闭