在PHP中测试变量存在的最佳方法;isset() 已明显损坏实际用例(带解决方案)不合理的用例,包括讨论

2022-08-30 06:26:04

isset() 文档中

isset() will return FALSE if testing a variable that has been set to NULL.

基本上,不检查变量是否被设置,而是是否设置为 除 .isset()NULL

鉴于此,实际检查变量是否存在的最佳方法是什么?我尝试了这样的东西:

if(isset($v) || @is_null($v))

(当未设置时,有必要避免警告),但具有与类似的问题:它在未设置的变量上返回!似乎还有:@$vis_null()isset()TRUE

@($v === NULL)

工作原理与 一模一样,所以这也出来了。@is_null($v)

我们应该如何可靠地检查PHP中变量的存在?


编辑:PHP中未设置的变量和设置为以下变量之间显然存在差异:NULL

<?php
$a = array('b' => NULL);
var_dump($a);

PHP 显示存在,并具有值。如果添加:$a['b']NULL

var_dump(isset($a['b']));
var_dump(isset($a['c']));

你可以看到我正在谈论的函数的模糊性。以下是所有这三个的输出:isset()var_dump()s

array(1) {
  ["b"]=>
  NULL
}
bool(false)
bool(false)

进一步编辑:两件事。

第一,一个用例。转换为 SQL 语句数据的数组,其中数组的键是表的列,数组的值是要应用于每列的值。表的任何列都可以包含一个值,通过在数组中传递一个值来表示。您需要一种方法来区分不存在的数组键和设置为 ;这就是不更新列的值和将列的值更新为 .UPDATENULLNULLNULLNULL

其次,Zoredache的答案对于我上面的用例和任何全局变量都是正确的:array_key_exists()

<?php
$a = NULL;
var_dump(array_key_exists('a', $GLOBALS));
var_dump(array_key_exists('b', $GLOBALS));

输出:

bool(true)
bool(false)

由于它几乎可以正确处理所有地方,我可以看到不存在的变量和设置为的变量之间存在任何歧义,我将array_key_exists()称为PHP中真正检查变量是否存在的官方最简单方法NULL

(我能想到的唯一另一种情况是类属性,对于类属性,根据其文档,它的工作原理与它正确区分未设置和设置为 .)property_exists()array_key_exists()NULL


答案 1

如果要检查的变量在全局范围内,则可以执行以下操作:

array_key_exists('v', $GLOBALS) 

答案 2

试图概述各种讨论和答案:

对于这个问题,没有一个单一的答案可以取代所有可以使用的设置方式。一些用例由其他功能解决,而另一些用例经不起审查,或者具有超出代码高尔夫的可疑价值。其他用例远非“破碎”或“不一致”,而是证明了为什么对逻辑行为的反应。issetnull

实际用例(带解决方案)

1. 阵列键

数组可以被视为变量的集合,并像对待它们一样对待它们。但是,由于可以对它们进行迭代、计数等,因此缺失值与值为 的缺失值不同。unsetissetnull

在这种情况下,答案是使用array_key_exists()而不是isset()。

由于这是将要检查的数组作为函数参数,因此如果数组本身不存在,PHP 仍将引发“通知”。在某些情况下,可以有效地争辩说,每个维度都应该首先初始化,因此通知正在做它的工作。对于其他情况,依次检查数组的每个维度的“递归”函数将避免这种情况,但基本上与 相同。它也与值的处理有些关系。array_key_exists@array_key_existsnull

2. 对象属性

在传统的“面向对象编程”理论中,封装和多态性是对象的关键属性;在基于类的 OOP 实现(如 PHP)中,封装的属性声明为类定义的一部分,并指定访问级别(、或 )。publicprotectedprivate

但是,PHP还允许您动态地向对象添加属性,就像您对数组的键一样,并且有些人以类似于关联数组的方式使用无类对象(从技术上讲,内置的实例,它没有方法或私有功能)。这会导致函数可能想知道是否已将特定属性添加到为其提供的对象中的情况。stdClass

与数组键一样,用于检查对象属性的解决方案包含在语言中,合理地称为property_exists

不合理的用例,包括讨论

3. 、全局命名空间的其他污染register_globals

该功能向全局范围添加了变量,其名称由 HTTP 请求的各个方面(GET 和 POST 参数以及 Cookie)确定。这可能会导致代码有错误和不安全,这就是为什么它自 PHP 4.2(2000 年 8 月发布)以来默认处于禁用状态,并在 2012 年 3 月发布的 PHP 5.4 中完全删除。但是,某些系统可能仍在启用或模拟此功能的情况下运行。还可以使用关键字或数组以其他方式“污染”全局命名空间。register_globalsglobal$GLOBALS

首先,它本身不太可能意外地产生变量,因为GET,POST和cookie值将始终是字符串(仍然从返回),并且会话中的变量应该完全在程序员的控制之下。register_globalsnull''trueisset

其次,如果变量覆盖了以前的一些初始化,则具有该值的污染只是一个问题。“覆盖”一个未初始化的变量,只有当其他地方的代码区分这两种状态时,才会有问题,所以这种可能性本身就是反对进行这种区分的论据。nullnull

4. 和get_defined_varscompact

PHP 中一些很少使用的函数(如 get_defined_varscompact)允许您将变量名称视为数组中的键。对于全局变量,超全局数组$GLOBALS允许类似的访问,并且更常见。如果未在相关作用域中定义变量,则这些访问方法的行为将有所不同。

一旦您决定使用这些机制之一将一组变量视为数组,就可以对它执行与对任何普通数组相同的所有操作。因此,见1。

仅用于预测这些函数即将如何行为的功能(例如,“返回的数组中会有一个关键的'foo'吗?”)是多余的,因为您可以简单地运行该函数并找出没有不良影响。get_defined_vars

4a.变量 ($$foo)

虽然与将一组变量转换为关联数组的函数并不完全相同,但大多数使用“变量变量”(“分配给基于该其他变量命名的变量”)的情况可以并且应该更改为使用关联数组。

从根本上说,变量名称是程序员给值的标签;如果在运行时确定它,它实际上不是一个标签,而是某个键值存储中的键。更实际的是,由于不使用数组,您将失去计数,迭代等功能;在键值存储的“外部”设置变量也可能变得不可能,因为它可能被 覆盖。$$foo

一旦更改为使用关联数组,代码将适用于解决方案 1。间接对象属性访问(例如 )可以通过解决方案 2 来解决。$foo->$property_name

5.比打字容易得多issetarray_key_exists

我不确定这是否真正相关,但是是的,PHP的函数名称有时可能非常冗长且不一致。显然,PHP的史前版本使用函数名称的长度作为哈希键,因此Rasmus故意编造函数名称,以便它们具有异常数量的字符......htmlspecialchars

不过,至少我们不是在写Java,对吧?;)

6. 未初始化的变量具有类型

有关变量基础知识的手册页包括以下语句:

未初始化的变量具有其类型的默认值,具体取决于使用它们的上下文

我不确定Zend引擎中是否存在“未初始化但已知类型”的概念,或者这是否对语句进行了过多的解读。

显而易见的是,它对它们的行为没有实际区别,因为该页面上描述的未初始化变量的行为与值为 的变量的行为相同。举个例子,在这个代码中,和 都以整数结束:null$a$b42

unset($a);
$a += 42;

$b = null;
$b += 42;

(第一个会引发有关未声明变量的通知,试图让你写出更好的代码,但这不会对代码的实际运行方式产生任何影响。

99. 检测函数是否已运行

(保持这个最后,因为它比其他的要长得多。也许我稍后会编辑它...)

请考虑以下代码:

$test_value = 'hello';
foreach ( $list_of_things as $thing ) {
    if ( some_test($thing, $test_value) ) {
        $result = some_function($thing);
    }
}
if ( isset($result) ) {
    echo 'The test passed at least once!';
}

如果可以返回,则即使返回,也有可能无法到达。程序员的意图是检测何时从未设置过,但PHP不允许他们这样做。some_functionnullechosome_testtrue$result

但是,此方法还存在其他问题,如果添加外部循环,这些问题就会变得很清楚:

foreach ( $list_of_tests as $test_value ) {
    // something's missing here...
    foreach ( $list_of_things as $thing ) {
        if ( some_test($thing, $test_value) ) {
            $result = some_function($thing);
        }
    }
    if ( isset($result) ) {
        echo 'The test passed at least once!';
    }
}

因为 从不显式初始化,所以当第一个测试通过时,它将采用一个值,从而无法判断后续测试是否通过。当变量未正确初始化时,这实际上是一个非常常见的错误。$result

为了解决这个问题,我们需要在我评论缺少某些东西的那条线上做一些事情。最明显的解决方案是设置为永不返回的“终端值”;如果这是 ,则代码的其余部分将正常工作。如果终端值没有自然的候选项,因为具有极其不可预测的返回类型(这本身可能是一个坏符号),则可以使用额外的布尔值,例如,可以使用。$resultsome_functionnullsome_function$found

思想实验一:常数very_null

从理论上讲,PHP可以提供一个特殊的常量 - 以及 - 在这里用作终端值;据推测,从函数返回此函数是非法的,或者它将被强制为 ,并且同样可能适用于将其作为函数参数传入。这将使这种非常具体的情况稍微简单一些,但是一旦您决定重构代码 - 例如,将内部循环放入单独的函数中 - 它将变得毫无用处。如果该常量可以在函数之间传递,则无法保证不会返回它,因此它将不再用作通用终端值。nullnullsome_function

在这种情况下,检测未初始化变量的参数可以归结为该特殊常量的参数:如果将注释替换为 ,并区别于 ,则引入的“值”无法传递,只能由特定的内置函数检测到。unset($result)$result = null$result

思想实验二:赋值计数器

另一种思考最后一个问题的方式是“有没有做过什么任务?与其将其视为 的特殊值,不如将其视为关于变量的“元数据”,有点像 Perl 的“变量污染”。所以,与其你可以叫它,而不是,。if$result$resultissethas_been_assigned_tounsetreset_assignment_state

但是,如果是这样,为什么要停留在布尔值上呢?如果您想知道测试通过了多少次,该怎么办?您可以简单地将元数据扩展到整数,并具有和...get_assignment_countreset_assignment_count

显然,添加这样的功能将在语言的复杂性和性能方面进行权衡,因此需要仔细权衡其预期的有用性。与常量一样,它只有在非常狭窄的情况下才有用,并且同样具有抗重构性。very_null

显而易见的问题是,为什么 PHP 运行时引擎应该提前假设您要跟踪此类事情,而不是让您使用普通代码显式执行此操作。


推荐