毛病1:foreach轮回后留下吊挂指针
在foreach轮回中,假如我们须要变动迭代的元素或是为了进步效力,应用援用是一个好要领:
$arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } // $arr is now array(2, 4, 6, 8)
这里有个题目许多人会含糊。轮回完毕后,value现实上是数组中末了一个元素的援用,如许在后续对$value的应用中,假如不晓得这一点,会激发一些莫名巧妙的毛病:)看看下面这段代码:
$array = [1, 2, 3]; echo implode(',', $array), "\n"; foreach ($array as &$value) {} // by reference echo implode(',', $array), "\n"; foreach ($array as $value) {} // by value (i.e., copy) echo implode(',', $array), "\n";
上面代码的运转效果以下:
你猜对了吗?为什么是这个效果呢?
我们来剖析下。第一个轮回事后,$value是数组中末了一个元素的援用。第二个轮回最先:
● 第一步:复制value(注重此时arr[2]的援用),这时候数组变成[1,2,1]
● 第二步:复制value,这时候数组变成[1,2,2]
● 第三步:复制value,这时候数组变成[1,2,2]
综上,终究效果就是1,2,2
防止这类毛病最好的要领就是在轮回后立即用unset函数烧毁变量:
$arr = array(1, 2, 3, 4); foreach ($arr as &$value) { $value = $value * 2; } unset($value); // $value no longer references $arr[3]
毛病2:对isset()函数行动的毛病邃晓
关于isset()函数,变量不存在时会返回false,变量值为null时也会返回false。这类行动很轻易把人弄含糊。。。看下面的代码:
$data = fetchRecordFromStorage($storage, $identifier); if (!isset($data['keyShouldBeSet']) { // do something here if 'keyShouldBeSet' is not set }
写这段代码的人本意多是假如data['keyShouldBeSet']已设置,但设置的值为null,照样会实行对应的逻辑,这就不符合代码的本意了。
下面是别的一个例子:
if ($_POST['active']) { $postData = extractSomething($_POST); } // ... if (!isset($postData)) { echo 'post not active'; }
上面的代码假定postData应当被设置,因而isset(postData)返回false的唯一门路就是$_POST['active']也返回false。
真是如许吗?固然不是!
纵然postData也有能够被设置为null,这时候isset($postData)就会返回false。这就不符合代码的本意了。
假如上面代码的本意仅是检测$_POST['active']是不是为真,下面如许完成会更好:
if ($_POST['active']) { $postData = extractSomething($_POST); } // ... if ($_POST['active']) { echo 'post not active'; }
推断一个变量是不是真正被设置(辨别未设置和设置值为null),array_key_exists()函数也许更好。重构上面的第一个例子,以下:
$data = fetchRecordFromStorage($storage, $identifier); if (! array_key_exists('keyShouldBeSet', $data)) { // do this if 'keyShouldBeSet' isn't set }
别的,连系get_defined_vars()函数,我们能够越发牢靠的检测变量在当前作用域内是不是被设置:
if (array_key_exists('varShouldBeSet', get_defined_vars())) { // variable $varShouldBeSet exists in current scope }
毛病3:殽杂返回值和返回援用
斟酌下面的代码:
class Config { private $values = []; public function getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];
运转上面的代码,将会输出下面的内容:
PHP Notice: Undefined index: test in /path/to/my/script.php on line 21
题目出在哪呢?题目就在于上面的代码殽杂了返回值和返回援用。在PHP中,除非你显现的指定返回援用,不然关于数组PHP是值返回,也就是数组的拷贝。因而上面代码对返回数组赋值,现实是对拷贝数组举行赋值,非原数组赋值。
// getValues() returns a COPY of the $values array, so this adds a 'test' element // to a COPY of the $values array, but not to the $values array itself. $config->getValues()['test'] = 'test'; // getValues() again returns ANOTHER COPY of the $values array, and THIS copy doesn't // contain a 'test' element (which is why we get the "undefined index" message). echo $config->getValues()['test'];
下面是一种能够的解决要领,输出拷贝的数组,而不是原数组:
$vals = $config->getValues(); $vals['test'] = 'test'; echo $vals['test'];
假如你就是想要转变原数组,也就是要反回数组援用,那应当怎样处置惩罚呢?要领就是显现指定返回援用即可:
class Config { private $values = []; // return a REFERENCE to the actual $values array public function &getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];
经由革新后,上面代码将会像你希冀那样会输出test。
我们再来看一个例子会让你更含糊的例子:
class Config { private $values; // using ArrayObject rather than array public function __construct() { $this->values = new ArrayObject(); } public function getValues() { return $this->values; } } $config = new Config(); $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];
假如你想的是会和上面一样输出“ Undefined index”毛病,那你就错了。代码会一般输出“test”。缘由在于PHP关于对象默许就是按援用返回的,而不是按值返回。
综上所述,我们在应用函数返回值时,要弄清晰是值返回照样援用返回。PHP中关于对象,默许是援用返回,数组和内置基本范例默许均按值返回。这个要与别的言语区分开来(许多言语关于数组是援用通报)。
像别的言语,比方java或C#,应用getter或setter来接见或设置类属性是一种更好的计划,固然PHP默许不支撑,须要本身完成:
class Config { private $values = []; public function setValue($key, $value) { $this->values[$key] = $value; } public function getValue($key) { return $this->values[$key]; } } $config = new Config(); $config->setValue('testKey', 'testValue'); echo $config->getValue('testKey'); // echos 'testValue'
上面的代码给挪用者能够接见或设置数组中的恣意值而不必授与数组public接见权限。觉得怎样:)
毛病4:在轮回中实行sql查询
在PHP编程中发明相似下面的代码并不少见:
$models = []; foreach ($inputValues as $inputValue) { $models[] = $valueRepository->findByValue($inputValue); }
固然上面的代码是没有什么毛病的。题目在于我们在迭代过程当中$valueRepository->findByValue()能够每次都实行了sql查询:
$result = $connection->query("SELECT `x`,`y` FROM `values` WHERE `value`=" . $inputValue);
假如迭代了10000次,那末你就离别实行了10000次sql查询。假如如许的剧本在多线程顺序中被挪用,那极能够你的体系就挂了。。。
在编写代码过程当中,你应当要清晰什么时候应当实行sql查询,只管一次sql查询掏出一切数据。
有一种营业场景,你极能够会犯上述毛病。假定一个表单提交了一系列值(假定为IDs),然后为了掏出一切ID对应的数据,代码将遍历IDs,离别对每一个ID实行sql查询,代码以下所示:
$data = []; foreach ($ids as $id) { $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` = " . $id); $data[] = $result->fetch_row(); }
但一样的目标能够在一个sql中越发高效的完成,代码以下:
$data = []; if (count($ids)) { $result = $connection->query("SELECT `x`, `y` FROM `values` WHERE `id` IN (" . implode(',', $ids)); while ($row = $result->fetch_row()) { $data[] = $row; } }
毛病5:内存应用低效和错觉
一次sql查询猎取多条纪录比每次查询猎取一条纪录效力肯定要高,但假如你应用的是php中的mysql扩大,那末一次猎取多条纪录就极能够会致使内存溢出。
我们能够写代码来试验下(测试环境: 512MB RAM、MySQL、php-cli):
// connect to mysql $connection = new mysqli('localhost', 'username', 'password', 'database'); // create table of 400 columns $query = 'CREATE TABLE `test`(`id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT'; for ($col = 0; $col < 400; $col++) { $query .= ", `col$col` CHAR(10) NOT NULL"; } $query .= ');'; $connection->query($query); // write 2 million rows for ($row = 0; $row < 2000000; $row++) { $query = "INSERT INTO `test` VALUES ($row"; for ($col = 0; $col < 400; $col++) { $query .= ', ' . mt_rand(1000000000, 9999999999); } $query .= ')'; $connection->query($query); }
如今来看看资本斲丧:
// connect to mysql $connection = new mysqli('localhost', 'username', 'password', 'database'); echo "Before: " . memory_get_peak_usage() . "\n"; $res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 1'); echo "Limit 1: " . memory_get_peak_usage() . "\n"; $res = $connection->query('SELECT `x`,`y` FROM `test` LIMIT 10000'); echo "Limit 10000: " . memory_get_peak_usage() . "\n";
输出效果以下:
Before: 224704 Limit 1: 224704 Limit 10000: 224704
依据内存应用量来看,貌似一切一般。为了越发肯定,试着一次猎取100000条纪录,效果顺序取得以下输出:
PHP Warning: mysqli::query(): (HY000/2013): Lost connection to MySQL server during query in /root/test.php on line 11
这是怎么回事呢?
题目出在php的mysql模块的工作方式,mysql模块现实上就是libmysqlclient的一个代办。在查询猎取多条纪录的同时,这些纪录会直接 保留在内存中。由于这块内存不属于php的内存模块所治理,所以我们挪用memory_get_peak_usage()函数所取得的值并不是实在应用内存 值,因而便涌现了上面的题目。
我们能够应用mysqlnd来替代mysql,mysqlnd编译为php本身扩大,其内存应用由php内存治理模块所掌握。假如我们用mysqlnd来完成上面的代码,则会越发实在的回响反映内存应用情况:
Before: 232048 Limit 1: 324952 Limit 10000: 32572912
越发蹩脚的是,依据php的官方文档,mysql扩大存储查询数据应用的内存是mysqlnd的两倍,因而本来的代码应用的内存是上面显现的两倍摆布。
为了防止此类题目,能够斟酌分频频完成查询,减小单次查询数据量:
$totalNumberToFetch = 10000; $portionSize = 100; for ($i = 0; $i <= ceil($totalNumberToFetch / $portionSize); $i++) { $limitFrom = $portionSize * $i; $res = $connection->query( "SELECT `x`,`y` FROM `test` LIMIT $limitFrom, $portionSize"); }
联络上面提到的毛病4能够看出,在现实的编码过程当中,要做到一种均衡,才既满足功用要求,又能保证机能。
毛病6:疏忽Unicode/UTF-8题目
php编程中,在处置惩罚非ascii字符时,会碰到一些题目,要很警惕的去看待,要不然就会毛病各处。举个简朴的例子,strlen(name),假如name包含非ascii字符,那效果就有些出人意料。在此给出一些发起,只管防止此类题目:
● 假如你对unicode和utf-8不是很相识,那末你最少应当相识一些基本。引荐浏览这篇文章。
● 最好应用mb_*函数来处置惩罚字符串,防止应用老的字符串处置惩罚函数。这里要确保PHP的“multibyte”扩大已开启。
● 数据库和表最好应用unicode编码。
● 晓得jason_code()函数会转换非ascii字符,但serialize()函数不会。
● php代码源文件最好应用不含bom的utf-8花样。
毛病7:假定$_POST老是包含POST数据
PHP中的$_POST并不是老是包含表单POST提交过来的数据。假定我们经由过程 jQuery.ajax() 要领向服务器发送了POST要求:
// js $.ajax({ url: 'http://my.site/some/path', method: 'post', data: JSON.stringify({a: 'a', b: 'b'}), contentType: 'application/json' });
注重代码中的 contentType: ‘application/json' ,我们是以json数据花样来发送的数据。在服务端,我们仅输出$_POST数组:
// php var_dump($_POST);
你会很惊异的发明,效果是下面所示:
array(0) { }
为什么是如许的效果呢?我们的json数据 {a: ‘a', b: ‘b'} 哪去了呢?
答案就是PHP仅仅剖析Content-Type为 application/x-www-form-urlencoded 或 multipart/form-data的Http要求。之所以如许是由于汗青缘由,PHP最初完成$_POST时,最盛行的就是上面两种范例。因而虽然说如今有些范例(比方application/json)很盛行,但PHP中照样没有去完成自动处置惩罚。
由于POST是全局变量,所以变动_POST会全局有用。因而关于Content-Type为 application/json 的要求,我们须要手工去剖析json数据,然后修正$_POST变量。
// php $_POST = json_decode(file_get_contents('php://input'), true);
此时,我们再去输出$_POST变量,则会取得我们希冀的输出:
array(2) { ["a"]=> string(1) "a" ["b"]=> string(1) "b" }
毛病8:以为PHP支撑字符数据范例
看看下面的代码,猜想下会输出什么:
for ($c = 'a'; $c <= 'z'; $c++) { echo $c . "\n"; }
假如你的回覆是输出'a'到'z',那末你会惊异的发明你的回覆是毛病的。
不错,上面的代码确实会输出'a'到'z',但除此之外,还会输出'aa'到'yz'。我们来剖析下为什么会是如许的效果。
在PHP中不存在char数据范例,只需string范例。邃晓这点,那末对'z'举行递增操纵,效果则为'aa'。关于字符串比较大小,学过C的应当都晓得,'aa'是小于'z'的。这也就诠释了为什么会有上面的输出效果。
假如我们想输出'a'到'z',下面的完成是一种不错的要领:
for ($i = ord('a'); $i <= ord('z'); $i++) { echo chr($i) . "\n"; }
或许如许也是OK的:
$letters = range('a', 'z'); for ($i = 0; $i < count($letters); $i++) { echo $letters[$i] . "\n"; }
毛病9:疏忽编码规范
虽然说疏忽编码规范不会致使毛病或是bug,但遵照肯定的编码规范照样很主要的。
没有一致的编码规范会使你的项目涌现许多题目。最显著的就是你的项目代码不具有一致性。更坏的处所在于,你的代码将越发难以调试、扩大和保护。这也就意味着你的团队效力会下降,包含做一些许多无意义的劳动。
关于PHP开辟者来讲,是比较荣幸的。由于有PHP编码规范引荐(PSR),由下面5个部份构成:
● PSR-0:自动加载规范
● PSR-1:基本编码规范
● PSR-2:编码作风指南
● PSR-3:日记接口规范
● PSR-4:自动加载
PSR最初由PHP社区的几个大的整体所建立并遵照。Zend, Drupal, Symfony, Joomla及别的的平台都为此规范做过孝敬并遵照这个规范。纵然是PEAR,早些年也想让本身成为一个规范,但如今也到场了PSR阵营。
在某些情况下,应用什么编码规范是可有可无的,只需你应用一种编码作风并一向对峙应用即可。然则遵照PSR规范不失为一个好要领,除非你有什么特别的缘由要 本身弄一套。如今越来越多的项目都最先应用PSR,大部份的PHP开辟者也在应用PSR,因而应用PSR会让新到场你团队的成员更快的熟习项目,写代码时 也会越发温馨。
毛病10:毛病应用empty()函数
一些PHP开辟人员喜好用empty()函数去对变量或表达式做布尔推断,但在某些情况下会让人很疑心。
起首我们来看看PHP中的数组Array和数组对象ArrayObject。看上去彷佛没什么区分,都是一样的。真的如许吗?
// PHP 5.0 or later: $array = []; var_dump(empty($array)); // outputs bool(true) $array = new ArrayObject(); var_dump(empty($array)); // outputs bool(false) // why don't these both produce the same output?
让事变变得更庞杂些,看看下面的代码:
// Prior to PHP 5.0: $array = []; var_dump(empty($array)); // outputs bool(false) $array = new ArrayObject(); var_dump(empty($array)); // outputs bool(false)
很不幸的是,上面这类要领很受迎接。比方,在Zend Framework 2中,Zend\Db\TableGateway 在 TableGateway::select() 效果集上挪用 current() 要领返回数据集时就是这么干的。开辟人员很轻易就会踩到这个坑。
为了防止这些题目,搜检一个数组是不是为空末了的要领是用 count() 函数:
// Note that this work in ALL versions of PHP (both pre and post 5.0): $array = []; var_dump(count($array)); // outputs int(0) $array = new ArrayObject(); var_dump(count($array)); // outputs int(0)
在这趁便提一下,由于PHP中会将数值0以为是布尔值false,因而 count() 函数能够直接用在 if 前提语句的前提推断中来推断数组是不是为空。别的,count() 函数关于数组来讲庞杂度为O(1),因而用 count() 函数是一个明智的挑选。
再来看一个用 empty() 函数很风险的例子。当在把戏要领 __get() 中连系应用 empty() 函数时,也是很风险的。我们来定义两个类,每一个类都有一个 test 属性。
起首我们定义 Regular 类,有一个 test 属性:
class Regular { public $test = 'value'; }
然后我们定义 Magic 类,并用 __get() 把戏要领来接见它的 test 属性:
class Magic { private $values = ['test' => 'value']; public function __get($key) { if (isset($this->values[$key])) { return $this->values[$key]; } } }
好了。我们如今来看看接见各个类的 test 属性会发作什么:
$regular = new Regular(); var_dump($regular->test); // outputs string(4) "value" $magic = new Magic(); var_dump($magic->test); // outputs string(4) "value"
到目前为止,都照样一般的,没有让我们觉得含糊。
但在 test 属性上应用 empty() 函数会怎样呢?
var_dump(empty($regular->test)); // outputs bool(false) var_dump(empty($magic->test)); // outputs bool(true)
效果是不是是很不测?
很不幸的是,假如一个类应用魔法 __get() 函数来接见类属性的值,没有简朴的要领来搜检属性值是不是为空或是不存在。在类作用域外,你只能搜检是不是返回 null 值,但这并不肯定意味着没有设置响应的键,由于键值能够被设置为 null 。
相比之下,假如我们接见 Regular 类的一个不存在的属性,则会取得一个相似下面的Notice音讯:
Notice: Undefined property: Regular::$nonExistantTest in /path/to/test.php on line 10 Call Stack: 0.0012 234704 1. {main}() /path/to/test.php:0
因而,关于 empty() 函数,我们要警惕的应用,要不然的话就会效果出人意料,以至潜伏的误导你。
以上就是php罕见毛病有哪些?的细致内容,更多请关注ki4网别的相干文章!