试图概述各种讨论和答案:
这个问题没有一个单一的答案可以取代所有isset
可以使用的方法。一些用例由其他功能解决,而另一些则经不起审查,或者具有超出代码高尔夫的可疑价值。其他用例远非“破碎”或“不一致”,而是说明了为什么isset
对 的反应null
是合乎逻辑的行为。
真实用例(带有解决方案)
1. 数组键
数组可以被当作变量的集合来对待,就像unset
对待isset
它们一样对待它们。但是,由于它们可以被迭代、计数等,缺失值与值为 的值不同null
。
在这种情况下,答案是使用array_key_exists()
而不是isset()
.
由于这是将数组作为函数参数进行检查,因此如果数组本身不存在,PHP 仍会发出“通知”。在某些情况下,可以有效地争论每个维度应该首先被初始化,所以通知正在完成它的工作。array_key_exists
对于其他情况,依次检查数组的每个维度的“递归”函数可以避免这种情况,但基本上与@array_key_exists
. 它也与null
值的处理有些相切。
2.对象属性
在传统的“面向对象编程”理论中,封装和多态是对象的关键属性;在 PHP 等基于类的 OOP 实现中,封装的属性被声明为类定义的一部分,并被赋予访问级别(public
、protected
或private
)。
但是,PHP 还允许您动态地向对象添加属性,就像您将键添加到数组一样,并且有些人以类似的方式使用无类对象(从技术上讲,内置的实例stdClass
,它没有方法或私有功能)关联数组的方法。这会导致函数可能想知道是否已将特定属性添加到给定对象的情况。
与数组键一样,用于检查对象属性的解决方案包含在语言中,称为property_exists
.
不合理的用例,有讨论
3. register_globals
、全局命名空间等污染
该register_globals
功能将变量添加到全局范围,其名称由 HTTP 请求的各个方面(GET 和 POST 参数以及 cookie)确定。这可能会导致错误和不安全的代码,这就是为什么它自PHP 4.2(2000 年 8 月发布)以来默认禁用并在PHP 5.4(2012 年 3 月发布)中完全删除的原因。但是,某些系统可能在启用或模拟此功能的情况下仍在运行。global
也可以使用关键字或$GLOBALS
数组以其他方式“污染”全局命名空间。
首先,register_globals
它本身不太可能意外产生null
变量,因为 GET、POST 和 cookie 值将始终是字符串(''
仍然返回true
from isset
),并且会话中的变量应该完全在程序员的控制之下。
其次,只有当这覆盖了一些先前的初始化时,变量与值的污染null
才是一个问题。仅当其他地方的代码区分这两种状态时, “覆盖”未初始化的变量null
才会有问题,因此这种可能性本身就是反对进行这种区分的论据。
4.get_defined_vars
和compact
PHP 中的一些很少使用的函数,例如get_defined_vars
and compact
,允许您将变量名视为数组中的键。对于全局变量,超全局数组$GLOBALS
允许类似的访问,并且更常见。如果变量未在相关范围内定义,这些访问方法的行为将有所不同。
一旦您决定使用其中一种机制将一组变量视为一个数组,您就可以对其执行所有与任何普通数组相同的操作。因此,见 1。
仅用于预测这些函数将如何运行的功能(例如,“在返回的数组中是否会有一个键 'foo' get_defined_vars
?”)是多余的,因为您可以简单地运行该函数并找出没有任何不良影响。
4a。变量变量 ( $$foo
)
虽然与将一组变量转换为关联数组的函数并不完全相同,但大多数使用“变量变量”(“分配给基于此其他变量命名的变量”)的情况可以并且应该改为使用关联数组.
从根本上说,变量名是程序员赋予值的标签。如果您在运行时确定它,它实际上并不是一个标签,而是某个键值存储中的一个键。更实际的是,如果不使用数组,您将失去计数、迭代等的能力;也可能无法在键值存储“外部”拥有一个变量,因为它可能会被$$foo
.
一旦更改为使用关联数组,代码将适用于解决方案 1。间接对象属性访问(例如$foo->$property_name
)可以使用解决方案 2 解决。
5.isset
比打字容易得多array_key_exists
我不确定这是否真的相关,但是是的,PHP 的函数名称有时会非常冗长且不一致。显然,PHP 的史前版本使用函数名的长度作为哈希键,所以 Rasmus 故意编造函数名,htmlspecialchars
这样它们就会有不寻常的字符数......
不过,至少我们不是在写 Java,对吧?;)
6.未初始化的变量有类型
变量基础的手册页包括以下语句:
未初始化的变量具有其类型的默认值,具体取决于使用它们的上下文
我不确定 Zend 引擎中是否存在“未初始化但已知类型”的概念,或者这是否对声明的解读过多。
很明显,这对它们的行为没有实际影响,因为该页面上描述的未初始化变量的行为与值为 的变量的行为相同null
。举一个例子,这段代码中的$a
和$b
都以整数结尾42
:
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!';
}
如果some_function
可以返回null
,则有可能echo
即使some_test
返回也无法到达true
。程序员的意图是检测何时$result
从未设置,但 PHP 不允许他们这样做。
但是,这种方法还有其他问题,如果添加外部循环,这些问题就会变得清晰:
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
从未显式初始化,所以当第一个测试通过时,它将采用一个值,因此无法判断后续测试是否通过。当变量没有正确初始化时,这实际上是一个非常常见的错误。
为了解决这个问题,我们需要在我评论说缺少某些东西的那一行做一些事情。最明显的解决方案是设置一个永远无法返回$result
的“终端值” ;some_function
如果是这样null
,那么其余代码将正常工作。如果由于some_function
具有极其不可预测的返回类型(这本身可能是一个不好的迹象)而没有自然的终端值候选者,那么可以使用额外的布尔值,例如$found
,来代替。
思想实验一:very_null
常数
PHP 理论上可以提供一个特殊的常量 - 以及null
- 在这里用作终端值;据推测,从函数返回 this 是非法的,或者它会被强制转换为null
,同样可能适用于将它作为函数参数传入。这将使这个非常具体的情况稍微简单一些,但是一旦您决定重构代码 - 例如,将内部循环放入单独的函数中 - 它就会变得无用。如果常量可以在函数之间传递,则不能保证some_function
不会返回它,因此它不再用作通用终端值。
在这种情况下,用于检测未初始化变量的论点归结为该特殊常量的论点:如果将注释替换为unset($result)
,并将其与 区别对待$result = null
,则您将引入一个“值”,$result
因为它不能传递,只能由特定的内置函数检测到。
思想实验二:赋值计数器
思考最后一个if
问题的另一种方式是“有什么任务$result
吗?” 与其将其视为 的特殊值,不如将其视为有关$result
变量的“元数据” ,有点像 Perl 的“变量污染”。所以不是你可能会调用它,而不是,。isset
has_been_assigned_to
unset
reset_assignment_state
但如果是这样,为什么要停在一个布尔值上?如果您想知道测试通过了多少次怎么办?您可以简单地将元数据扩展为整数并拥有get_assignment_count
和reset_assignment_count
...
显然,添加这样的功能会在语言的复杂性和性能上进行权衡,因此需要仔细权衡其预期的有用性。与very_null
常数一样,它仅在非常狭窄的情况下才有用,并且类似地抵抗重构。
希望显而易见的问题是,为什么 PHP 运行时引擎应该提前假设您想要跟踪这些事情,而不是让您使用普通代码明确地进行。