1. 首页
  2. >
  3. 编程技术
  4. >
  5. PHP

PHP高级编程-回归原生态-空与非空

第 4 章 回归原生态


就当前而言,PHP仍然是网站建设的主流编程语言之一。一方面,是得益于它自身的简单性,容易学习且快速上手;另一方面,得益于开源社区贡献的各种优秀框架、类库和项目。这些源代码下载到服务器后,简单配置一下,甚至都不需要二次开发就能直接使用,非常方便。


但需要注意的是,别人提供、贡献的开源项目是可以减少我们重复开发的成本,并不意味着我们对原生态的PHP就可以置之不理。正好相反,更深入地理解PHP原生态的用法,将能帮助我们从底层、从根本上更透彻地理解和掌握别人封装的类、函数、模块和扩展。也就是说,除了会使用,还不足矣。作为专业的开发人士,我们还应明白为什么会这样,洞明背后微妙的差异,对中大型项目开发尤其关键。关于这部分,后面会慢慢说到。


PHP开发,入门很简单,但要深入和精通很难,需要付出一定的时间以及精力。在这一章节,肯定不能罗列PHP这门语言全部的要点,只是通过分享一些常见的或者有代表性的知识点,希望能引起广大开发同学对原生态的PHP有更多的关注,加深理解。做到:不误解,不误用,更不误导。


4.1 空与非空

很多时候,对于明显的初级PHP语法,我们一眼就能识别。假设稍微转换一下,这时就需要花点心思才能识破其中的奥妙。最困难的莫过于,微妙的用法一旦与繁杂的业务代码、规则逻辑混在一起,散落在上千行代码内时,想要在短时间内发现问题所在则是个巨大的挑战。


4.1.1 简单的判空

大家使用最多的PHP函数之一,也许是empty()这个函数了。而且,大家都知道,什么样的情况下,一个值会判断为空。摘自官方文档的说明,以下的东西被认为是空的:

  • "" (空字符串)
  • 0 (作为整数的0)
  • 0.0 (作为浮点数的0)
  • "0" (作为字符串的0)
  • NULL
  • FALSE
  • array() (一个空数组)
  • $var; (一个声明了,但是没有值的变量)


非常容易看出,以下代码输出的结果为true。

<?php $var = 0; var_dump(empty($var)); // 结果输出为true


4.1.2 隐晦的判空

但是,结合其他函数一起使用时,并且不再是直接使用empty()函数来判断时,情况就开始变得晦涩了。一起来看下以下这段有问题的代码。看你需要多少时间才能发现里面的BUG?

<?php // 文章内容 $text = '软件开发是根据用户要求建造出软件系统或者系统中的软件部分的过程。……';   // 用户输入的关键字 $keyword = '';     $pos = strpos($text, $keyword);if ($pos) {     // 找到了,是我感兴趣的文章} else {     // 未能找到关键字 }


这里的场景是,根据用户输入的关键字,匹配某篇文章是不是读者感兴趣的。如果文章包含关键字就视为是用户感兴趣的,否则就是不感兴趣的。正常情况下这段代码是可以正常工作的,问题在于,如果碰巧用户输入的关键字刚好出现在文章开头时,就会发生意想不到的事情。例如,用户输入关键字“软件”。由于“软件”这个词出现在最前面,所以查找到的位置$pos值为0。最后在判断位置时,0被当作FALSE,即:$pos = !empty($pos) = !empty(0) = !TRUE = FALSE。实际是找到匹配值,却被误判断为未找到。由此就出现了一个BUG,引发了一个故障。


4.1.3 两个引申

说到这里,我们可以引申出两个有意义的讨论。第一是关于PHP全等判断,第二是关于PHP函数的错误返回。


PHP全等判断

这点很好理解,从我们开始接触PHP编程时,就已经知道这一点了。全等判断是指不仅要求值相等,而且还要求类型也一样。即在不进行隐式类型转换的情况下,待比较的这两个值是否仍然相等。普通的相等,PHP默认会进行隐式的转换,使用两个等号 == 表示。全等判断则用三个等号 === 表示。


针对前面刚才的问题,可以使用全等来修正对是否找到这一逻辑的判断。即:

if ($pos !== FALSE) {     // 找到了,是我感兴趣的文章} else {     // 未能找到关键字 }


这一点是大家都知道的,但下面这一点也许大家都知道,但容易忽略。


PHP函数的错误返回

PHP底层的代码,可以分为两大类。一类是早期面向过程范式的函数,例如:strpos()、json_decode()、curl_exec()、file_get_contents()等函数。另一类是后期面向对象范式的类与对象,例如:PDO、Memcached、SoapClient等。对于前者,当失败时,例如字符串查找不到、JSON解码失败、URL抓取超时或者文件不存在时,所调用的函数会返回布尔值FALSE表示失败。而对于后者,即若使用的是封装的类,并通过类实例化的对象来操作时,当发生失败或者异常情况时,则会直接抛出异常。例如数据库连接失败、SOAP调用失败。


这可以说是PHP语言的惯例。当调用函数并失败时,返回FALSE,这里需要做好全等判断,避免与正常情况下的数字0、"" (空字符串)或"0" (作为字符串的0)混淆。前面提到的关键字位置查找就是其中一例,又如文件内容的读取,若文件存在但读取的内容为空字符串,与文件不存在读取到的结果为FALSE,这两者之间是有着微妙的区别的。在进行项目开发时,一定要注意严格区分,否则就会失之毫厘,谬以千里。


此外,如果是函数失败了,可以通过提供的错误码查找到对应的错误信息。例如,使用curl失败时,可以使用curl_error()函数查看错误信息。例如官网提供的示例:

<?php // 创建 cURL 句柄,指向一个不存在的位置 $ch = curl_init('http://404.php.net/'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);// 笔者注,使用全等判断 if(curl_exec($ch) === false){     // 笔者注,获取错误信息     echo 'Curl error: ' . curl_error($ch); }


4.1.4 为什么系统崩溃了?

再来看一个更为复杂的真实案例场景。在一个中型社交游戏系统中,有一个业务场景是需要获取每个用户的道具数量。相关代码片段如下:

<?php // 用户名 $user = 'dogstar';// 缓存KEY $key = 'item_' . $user;// 缓存读取 $cache = new Cache\Memcached(); $num = $cache->get($key); if ($num <= 0) {     // 查询数据库:SELECT COUNT(id) FROM user_items WHERE username = 'dogstar'     $model = new Model\Item();     $num = $model->getUserItemTotalNum($user);       $cache->set($key, $num, 600); }// 更多业务处理……


在上面代码中, 对于用户名为dogstar的游戏玩家,先会从Memcache缓存中获取他的道具总数,如果之前没有缓存,则再从数据库查询该用户的道具总数。最后写入到缓存,避免下一次重复穿透读取数据库,从而减轻数据库的访问压力。


但该功能上线不久后,发现整个游戏系统就崩溃了,经排查发现是数据库负载过高导致系统无法正常响应。原来,有很多玩家用户是没有任何道具的,即他们的道具数量为0。当没有缓存时,通过Memcached读取出来的值是FALSE,经隐式转换后也是0。此时,就无法区分是用户真的没有道具,还是因为没有缓存需要直接查询数据库。最终导致了在高并发的情况下,频繁穿透到数据库,进行聚合的查询,拖垮了数据库服务器。


这时的情况,会比以往都要严峻。首先,这里不仅有前面讨论的空判断,还引入了Memcached缓存和数据库查询。其次,这是确切发生在线上环境的故障问题,每一秒都在影响游戏玩家的用户体验,每一分钟都对我们的产品造成了损失,需要在面临重大压力、在最短的时间内找出原因并修复上线。最后,上面的代码片段是经过简化提炼的代码,实际情况上,代码可能遍布在你的项目里,更为要命的是,你的项目拥有10万行以上的代码!


对于这种情况,需要提前意识到会发生怎样的状况。改进的方式很简单,一种是沿用前面的全等判断;另一种就是,不要在缓存结果中只保存基本类型的数据,而是保存一个结构体,即保存一个数组到缓存中。如:

if (empty($data)) {     // 查询数据库:SELECT COUNT(id) FROM user_items WHERE username = 'dogstar'     $model = new Model\Item();     $num = $model->getUserItemTotalNum($user);       // 改进方案:保存一个数组到缓存     $data = array('num' => $num);     $cache->set($key, $data, 600); }   $num = $data['num'];


4.1.5 小结

作为三大程序控制结构之一,选择控制结构是我们平时项目开发过程中接触最多的。这里的条件逻辑判断,又会涉及对空和非空的判断。如果做出准确无误的判断,就要求我们对全等判断、各函数失败时的返回值、隐式类型转换、空值的判断等都要有一个全面的认识和清晰的理解。这样才不会误解、误用、误判。


关于空值判断,暂且讨论到这里。下面我们再来看下另一个在PHP开发中经常用到的数组。