原文链接:http://www.orlion.ga/253/

PHP是弱类型语言,向方法传递参数时也并不严格检查数据类型。不过有时候需要判断传递到方法中的参数,为此PHP中提供了一些函数来判断数据的类型,比如is_numeric()判断是否是一个数值或者可转换为数值的字符串,比如用于判断对象的类型运算符:instance of。instance of用来测定一个给定的对象是否来自指定的对象类。instance of运算符是PHP5引进的,在此之前是使用is_a()。

为了避免对象类型不规范引起的问题 ,PHP5中引入了类型提示这个概念,在定义方法参数时,同时定义参数的数据类型,如果在调用的时候,传入参数的类型与定义的参数类型不符,则会报错。这样就可以过滤对象的类型。

示例:

function array_print(Array $arr) {
    print_r($arr);
}
 
array_print(1);

上边的代码会报错,因为函数的参数指定为Array但是传入的是一个整型数据。当我们传入一个数组时则会正常运行,这是怎么实现的呢? 不管是在类中的方法还是我们调用的函数,都是使用function关键字作为其声明的标记,而类型提示的实现是在与函数的声明相关的,在声明时就已经确定了参数的类型是哪些,但是需要在调用时才会显示出来,这里我们从两个方面说明类型提示的实现:

      1. 参数声明时的类型提示

      2. 函数或方法调用时的类型提示

将刚才的例子改一下:

function array_print(Array $arr = 1) {
    print_r($arr);
}
 
array_print(array(1));

这个例子中函数的默认参数是一个整数,会报错。为什么会看到报错呢?因为默认值的检测过程发生在中间代码生成阶段,与运行时的报错不同,它还没有生成中间代码,也没有执行中间代码的过程。在Zend/zend_language_parse.y文件中,我们找到函数的参数列表在编译时都会调用zend_do_receive_arg函数。而在这个函数的参数列表中,第5个参数(znode *class_type)与本文所要表述的类型提示密切相关。这个参数的作用是声明类型提示中的类型,这里的类型有三种:

(1)空,即没有类型提示

(2)类型,用户定义或PHP自定义的类、接口等

(3)数组,编译期间对应的token是T_ARRAY,即Array字符

在zend_do_receive_arg函数中,针对class_type参数做了一系列的操作,基本上是针对上面列出的三种类型,其中对于类名,程序并没有判断这个类是否存在,即使使用了一个不存在的类名,程序在报错时显示的也是实参所给的对象并不是给定类的实例。

以上是声明类型提示的过程以及在声明过程中对参数默认值的判断过程,下面我们看下在函数或方法调用时类型提示的实现。

从上面的声明过程我们知道PHP在编译类型提示的代码时调用的是Zend/zend_complie.c文件中的zend_do_receive_arg函数,在这个函数中将类型提示的判断的opcode被赋值为ZEND_RECV。根据opcode的映射计算规则得出其在执行时调用的是ZEND_RECV_SPEC_HANDLER.其代码如下:

static int ZEND_FASTCALL  ZEND_RECV_SPEC_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
       ...// 省略
        if (param == NULL) {
                char *space;
                char *class_name = get_active_class_name(&space TSRMLS_CC);
                zend_execute_data *ptr = EX(prev_execute_data);
 
                if (zend_verify_arg_type((zend_function *) EG(active_op_array), 
arg_num, NULL, opline->extended_value TSRMLS_CC)) {
        ...//˯省略
                }
               ...//˯省略
        } else {
              ...//˯省略
                zend_verify_arg_type((zend_function *) EG(active_op_array), 
arg_num, *param, opline->extended_value TSRMLS_CC);
              ...//˯省略
        }
      ...//˯省略
}

如上所示,在ZEND_RECV_SPEC_HANDLER中最后调用的是zend_verify_arg_type。其代码如下:

static inline int zend_verify_arg_type(zend_function *zf, zend_uint arg_num, 
zval *arg, ulong fetch_type TSRMLS_DC)
{
   ...//˯省略
 
    if (cur_arg_info->class_name) {
        const char *class_name;
 
        if (!arg) {
            need_msg = zend_verify_arg_class_kind(cur_arg_info, fetch_type, 
&class_name, &ce TSRMLS_CC);
            return zend_verify_arg_error(zf, arg_num, cur_arg_info, need_msg, 
class_name, "none", "" TSRMLS_CC);
        }
        if (Z_TYPE_P(arg) == IS_OBJECT) { // 既然是类对象参数,传递的参数需要时对象类型
            // 下面检查这个对象是否是参数提示类的实例对象,这里是允许传递子类实例对象
            need_msg = zend_verify_arg_class_kind(cur_arg_info, fetch_type, 
&class_name, &ce TSRMLS_CC);
            if (!ce || !instanceof_function(Z_OBJCE_P(arg), ce TSRMLS_CC)) {
                return zend_verify_arg_error(zf, arg_num, cur_arg_info, 
need_msg, class_name, "instance of ", Z_OBJCE_P(arg)->name TSRMLS_CC);
            }
        } else if (Z_TYPE_P(arg) != IS_NULL || !cur_arg_info->allow_null) { // 参数为NULL,也是可以通过检查的 如果函数定义了参数默认值,不传递参数调用也是可以通过检查的             need_msg = zend_verify_arg_class_kind(cur_arg_info, fetch_type, 
&class_name, &ce TSRMLS_CC);
            return zend_verify_arg_error(zf, arg_num, cur_arg_info, need_msg, 
class_name, zend_zval_type_name(arg), "" TSRMLS_CC);
        }
    } else if (cur_arg_info->array_type_hint) { // 数组
        if (!arg) {
            return zend_verify_arg_error(zf, arg_num, cur_arg_info, "be an 
array", "", "none", "" TSRMLS_CC);
        }
         }
        if (Z_TYPE_P(arg) != IS_ARRAY && (Z_TYPE_P(arg) != IS_NULL || 
!cur_arg_info->allow_null)) {
            return zend_verify_arg_error(zf, arg_num, cur_arg_info, "be an 
array", "", zend_zval_type_name(arg), "" TSRMLS_CC);
        }
    }
    return 1;
}

zend_verify_arg_type的整个流程如图所示

如果类型提示报错,zend_verify_arg_type函数最后都会调用zend_verify_arg_class_kind生成报错信息,并且调用zend_verify_arg_error报错。如下所示代码:

static inline char * zend_verify_arg_class_kind(const zend_arg_info 
*cur_arg_info, ulong fetch_type, const char **class_name, zend_class_entry 
**pce TSRMLS_DC)
{
    *pce = zend_fetch_class(cur_arg_info->class_name, cur_arg_info-
>class_name_len, (fetch_type | ZEND_FETCH_CLASS_AUTO | 
ZEND_FETCH_CLASS_NO_AUTOLOAD) TSRMLS_CC);
 
    *class_name = (*pce) ? (*pce)->name: cur_arg_info->class_name;
    if (*pce && (*pce)->ce_flags & ZEND_ACC_INTERFACE) {
        return "implement interface ";
    } else {
        return "be an instance of ";
    }
}
 
 
static inline int zend_verify_arg_error(const zend_function *zf, zend_uint 
arg_num, const zend_arg_info *cur_arg_info, const char *need_msg, const char 
*need_kind, const char *given_msg, char *given_kind TSRMLS_DC)
{
    zend_execute_data *ptr = EG(current_execute_data)->prev_execute_data;
    char *fname = zf->common.function_name;
    char *fsep;
     char *fclass;
 
    if (zf->common.scope) {
        fsep =  "::";
        fclass = zf->common.scope->name;
    } else {
        fsep =  "";
        fclass = "";
    }
 
    if (ptr && ptr->op_array) {
        zend_error(E_RECOVERABLE_ERROR, "Argument %d passed to %s%s%s() must 
%s%s, %s%s given, called in %s on line %d and defined", arg_num, fclass, fsep, 
fname, need_msg, need_kind, given_msg, given_kind, ptr->op_array->filename, 
ptr->opline->lineno);
    } else {
        zend_error(E_RECOVERABLE_ERROR, "Argument %d passed to %s%s%s() must 
%s%s, %s%s given", arg_num, fclass, fsep, fname, need_msg, need_kind, 
given_msg, given_kind);
    }
    return 0;
}

在上面的代码中,我们可以看到前面的报错信息中的一些关键字Argument、passed to、called in等。这就是我们在调用函数或方法时类型提示显示错误信息的最终执行位置。

深入理解PHP内核(十)变量及数据类型-类型提示的实现的更多相关文章

  1. 深入理解PHP内核(九)变量及数据类型-静态变量

    原文链接:http://www.orlion.ga/251/ 通常静态变量是静态分配的,他们的生命周期和程序的生命周期一样长,只有在程序退出后才结束生命周期,这和局部变量相反,有的语言中全局变量也是静 ...

  2. 深入理解PHP内核(五)变量及数据类型-变量的结构和类型

    原文链接:http://www.orlion.ga/238/ 编程语言的类型可以分为强类型和弱类型两种,PHP是弱类型语言,但是C语言是强类型语言.在官网PHP实现内部,所有变量使用同一种数据结构(z ...

  3. [PHP] 深入理解PHP内核:变量及数据类型

    1.现实生活中我们会找一个小箱子来存放物品,一来显得不那么凌乱,二来方便以后找到.计算机也是这个道理,我们需要先在内存中找一块区域,规定用它来存放数据,并起一个好记的名字,方便以后查找.这块区域就是“ ...

  4. 深入理解PHP内核(八)变量及数据类型-预定义变量

    原文链接:http://www.orlion.ga/249/ PHP脚本在执行的时候用户全局变量(在用户空间显示定义的变量)会保存在一个HashTable数据类型的符号表中(symbol_table) ...

  5. 深入理解PHP内核(七)变量及数据类型-常量

    原文链接:http://www.orlion.ga/246/ 在PHP中,常量的名字是一个简单值的标识符,在脚本执行期间该值不能改变.和变量一样,常量默认为大小写敏感,但是通常是大写的. 常量是在变量 ...

  6. 深入理解PHP内核(十二)函数-函数的定义、传参及返回值

    原文链接:http://www.orlion.ga/344/ 一.函数的定义 用户函数的定义从function 关键字开始,如下 function foo($var) {    echo $var; ...

  7. 深入理解PHP内核(十四)类的成员变量及方法

    原文链接:http://www.orlion.ga/1237/ 类的成员变量在PHP中本质是一个变量,只是这些变量都归属于某个类,并且给这些变量是有访问控制的. 类的成员方法在PHP中本质是一个函数, ...

  8. Java定义接口变量为接收类型有什么好处(面向接口编程)

    个人理解:定义接口变量为接收类型属于面向接口的编程,通过接口的抽象能减少类之间的耦合,增加可复用性. 面向接口编程: 一种规范约束 制定者(或者叫协调者),实现者(或者叫生产者),调用者(或者叫消费者 ...

  9. PHP变量和数据类型

    编程语言可以分为三大类 1. 静态类型语言,比如:C/Java等,在静态语言类型中,类型的检查是在编译期(compile-time)确定的. 2. 动态语言类型,比如:PHP,python等各种脚本语 ...

随机推荐

  1. java代码中获取classpath路径

    Javaweb工程中,有时候需要自己手动的去读取classpath下面的配置文件,这里总结一点读取classpath路径的方法,分享一下. 方法一: String path = Test.class. ...

  2. JavaScript 基础第八天(DOM第二天)

    一.引言 初步认识DOM有可能会被各种不熟悉的因为因素影响自己的学习心态,你需要的是多去记忆一些单词然后加强自己的代码量. 二.导入 在昨天初步认识DOM以后我们见天将接着介绍有关于DOM的内容. 三 ...

  3. 转:给 C# 开发者的代码审查清单

      给 C# 开发者的代码审查清单   [感谢@L就是L 的热心翻译.如果其他朋友也有不错的原创或译文,可以尝试推荐给伯乐在线.] 这是为C#开发者准备的通用性代码审查清单,可以当做开发过程中的参考. ...

  4. FLEXNET License管理

    之前的程序License管理是我自己手撸的一个非常简单的东东,根据用户机器的MAC地址生成一个字串,程序执行的时候去比较这个字串.当时只是追求一个最简单的实现,像证书过期.功能点证书自然没有.这次新版 ...

  5. SQL入门经典(七) 之脚本和批处理

    什么是脚本.我们前面学的CREATE TABLE <table name> ,USE <database name>这些都是脚本,为什么用脚本.脚本存储到文件中并且可以重复利用 ...

  6. 找回忘记的Ubuntu用户名和密码

    找回忘记的Ubuntu用户名和密码 前端时间使用VMWare安装了个Ubuntn的虚拟机,但是烦于安装后显卡驱动的问题,看着操作界面就有点厌烦,就扔下了.今天打开虚拟机登陆的时候忘了密码,寻思着难道要 ...

  7. PostgreSQL基础整理(一)

    1. 创建数据库: 1)登录bin目录,createdb.exe -U postgres -e mydb; -U 表示本次操作的登录用户名,如果不写会取windows登录的账户,如Administra ...

  8. 分享一个简单程序(webApi+castle+Automapper+Ef+angular)

    前段时间在周末给朋友做了一个小程序,用来记录他们单位的一些调度信息(免费,无版权问题).把代码分享出来.整个程序没有做任何架构.但是麻雀虽小,用到的技术也没少.WebApi+Castle+AutoMa ...

  9. Java多线程17:中断机制

    概述 之前讲解Thread类中方法的时候,interrupt().interrupted().isInterrupted()三个方法没有讲得很清楚,只是提了一下.现在把这三个方法同一放到这里来讲,因为 ...

  10. [FPGA] 2、新建并运行一个工程

    上一篇将开发板的情况大致介绍了一下,这次将一步一步展示如何新建.调试并下载运行一个点亮LED的工程. 1)打开Quartus新建工程: 2)填写规则大致如下: 3)选择我们芯片的类型: 4)点击fil ...