站長資訊網
        最全最豐富的資訊網站

        PHP程序員容易犯的10個錯誤(分享)

        本篇文章給大家介紹10個 PHP 開發者最容易犯的錯誤。有一定的參考價值,有需要的朋友可以參考一下,希望對大家有所幫助。

        PHP程序員容易犯的10個錯誤(分享)

        PHP 語言讓 WEB 端程序設計變得簡單,這也是它能流行起來的原因。但也是因為它的簡單,PHP 也慢慢發展成一個相對復雜的語言,層出不窮的框架,各種語言特性和版本差異都時常讓搞的我們頭大,不得不浪費大量時間去調試。這篇文章列出了十個最容易出錯的地方,值得我們去注意。

        易犯錯誤 #1: 在 foreach循環后留下數組的引用

        還不清楚 PHP 中 foreach 遍歷的工作原理?如果你在想遍歷數組時操作數組中每個元素,在 foreach 循環中使用引用會十分方便,例如

        $arr = array(1, 2, 3, 4); foreach ($arr as &$value) {         $value = $value * 2; } // $arr 現在是 array(2, 4, 6, 8)

        問題是,如果你不注意的話這會導致一些意想不到的負面作用。在上述例子,在代碼執行完以后,$value 仍保留在作用域內,并保留著對數組最后一個元素的引用。之后與 $value 相關的操作會無意中修改數組中最后一個元素的值。

        你要記住 foreach 并不會產生一個塊級作用域。因此,在上面例子中 $value 是一個全局引用變量。在 foreach 遍歷中,每一次迭代都會形成一個對 $arr 下一個元素的引用。當遍歷結束后, $value 會引用 $arr 的最后一個元素,并保留在作用域中

        這種行為會導致一些不易發現的,令人困惑的bug,以下是一個例子

        $array = [1, 2, 3]; echo implode(',', $array), "n";  foreach ($array as &$value) {}    // 通過引用遍歷 echo implode(',', $array), "n";  foreach ($array as $value) {}     // 通過賦值遍歷 echo implode(',', $array), "n";

        以上代碼會輸出

        1,2,3 1,2,3 1,2,2

        你沒有看錯,最后一行的最后一個值是 2 ,而不是 3 ,為什么?

        在完成第一個 foreach 遍歷后, $array 并沒有改變,但是像上述解釋的那樣, $value 留下了一個對 $array 最后一個元素的危險的引用(因為 foreach 通過引用獲得 $value

        這導致當運行到第二個 foreach ,這個"奇怪的東西"發生了。當 $value 通過賦值獲得, foreach 按順序復制每個 $array 的元素到 $value 時,第二個 foreach 里面的細節是這樣的

        • 第一步:復制 $array[0] (也就是 1 )到 $value$value 其實是 $array最后一個元素的引用,即 $array[2]),所以 $array[2] 現在等于 1。所以 $array 現在包含 [1, 2, 1]
        • 第二步:復制 $array[1](也就是 2 )到 $value$array[2] 的引用),所以 $array[2] 現在等于 2。所以 $array 現在包含 [1, 2, 2]
        • 第三步:復制 $array[2](現在等于 2 ) 到 $value$array[2] 的引用),所以 $array[2] 現在等于 2 。所以 $array 現在包含 [1, 2, 2]

        為了在 foreach 中方便的使用引用而免遭這種麻煩,請在 foreach 執行完畢后 unset() 掉這個保留著引用的變量。例如

        $arr = array(1, 2, 3, 4); foreach ($arr as &$value) {     $value = $value * 2; } unset($value);   // $value 不再引用 $arr[3]

        常見錯誤 #2: 誤解 isset() 的行為

        盡管名字叫 isset,但是 isset() 不僅會在變量不存在的時候返回 false,在變量值為 null 的時候也會返回 false

        這種行為比最初出現的問題更為棘手,同時也是一種常見的錯誤源。

        看看下面的代碼:

        $data = fetchRecordFromStorage($storage, $identifier); if (!isset($data['keyShouldBeSet']) {     // do something here if 'keyShouldBeSet' is not set }

        開發者想必是想確認 keyShouldBeSet 是否存在于 $data 中。然而,正如上面說的,如果 $data['keyShouldBeSet'] 存在并且值為 null 的時候, isset($data['keyShouldBeSet']) 也會返回 false。所以上面的邏輯是不嚴謹的。

        我們來看另外一個例子:

        if ($_POST['active']) {     $postData = extractSomething($_POST); }  // ...  if (!isset($postData)) {     echo 'post not active'; }

        上述代碼,通常認為,假如 $_POST['active'] 返回 true,那么 postData 必將存在,因此 isset($postData) 也將返回 true。反之, isset($postData) 返回 false 的唯一可能是 $_POST['active'] 也返回 false

        然而事實并非如此!

        如我所言,如果$postData 存在且被設置為 nullisset($postData) 也會返回 false 。 也就是說,即使 $_POST['active'] 返回 trueisset($postData) 也可能會返回 false 。 再一次說明上面的邏輯不嚴謹。

        順便一提,如果上面代碼的意圖真的是再次確認 $_POST['active'] 是否返回 true,依賴 isset() 來做,不管對于哪種場景來說都是一種糟糕的決定。更好的做法是再次檢查 $_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 }

        另外,通過結合 array_key_exists()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() 的調用將會返回 $values 數組的一份拷貝,而不是對它的引用。考慮到這一點,讓我們重新回顧一下以上例子中的兩個關鍵行:

        // getValues() 返回了一個 $values 數組的拷貝 // 所以`test`元素被添加到了這個拷貝中,而不是 $values 數組本身。 $config->getValues()['test'] = 'test';   // getValues() 又返回了另一份 $values 數組的拷貝 // 且這份拷貝中并不包含一個`test`元素(這就是為什么我們會得到 「未定義索引」 消息)。 echo $config->getValues()['test'];

        一個可能的修改方法是存儲第一次通過 getValues() 返回的 $values 數組拷貝,然后后續操作都在那份拷貝上進行;例如:

        $vals = $config->getValues(); $vals['test'] = 'test'; echo $vals['test'];

        這段代碼將會正常工作(例如,它將會輸出test而不會產生任何「未定義索引」消息),但是這個方法可能并不能滿足你的需求。特別是上面的代碼并不會修改原始的$values數組。如果你想要修改原始的數組(例如添加一個test元素),就需要修改getValues()函數,讓它返回一個$values數組自身的引用。通過在函數名前面添加一個&來說明這個函數將返回一個引用;例如:

        class Config {     private $values = [];      // 返回一個 $values 數組的引用     public function &getValues() {         return $this->values;     } }  $config = new Config();  $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];

        這會輸出期待的test

        但是現在讓事情更困惑一些,請考慮下面的代碼片段:

        class Config {     private $values;      // 使用數組對象而不是數組     public function __construct() {         $this->values = new ArrayObject();     }      public function getValues() {         return $this->values;     } }  $config = new Config();  $config->getValues()['test'] = 'test'; echo $config->getValues()['test'];

        如果你認為這段代碼會導致與之前的數組例子一樣的「未定義索引」錯誤,那就錯了。實際上,這段代碼將會正常運行。原因是,與數組不同,PHP 永遠會將對象按引用傳遞。(ArrayObject 是一個 SPL 對象,它完全模仿數組的用法,但是卻是以對象來工作。)

        像以上例子說明的,你應該以引用還是拷貝來處理通常不是很明顯就能看出來。因此,理解這些默認的行為(例如,變量和數組以值傳遞;對象以引用傳遞)并且仔細查看你將要調用的函數 API 文檔,看看它是返回一個值,數組的拷貝,數組的引用或是對象的引用是必要的。

        盡管如此,我們要認識到應該盡量避免返回一個數組或 ArrayObject,因為這會讓調用者能夠修改實例對象的私有數據。這就破壞了對象的封裝性。所以最好的方式是使用傳統的「getters」和「setters」,例如:

        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');    // 輸出『testValue』

        這個方法讓調用者可以在不對私有的$values數組本身進行公開訪問的情況下設置或者獲取數組中的任意值。

        常見的錯誤 #4:在循環中執行查詢

        如果像這樣的話,一定不難見到你的 PHP 無法正常工作。

        $models = [];  foreach ($inputValues as $inputValue) {     $models[] = $valueRepository->findByValue($inputValue); }

        這里也許沒有真正的錯誤, 但是如果你跟隨著代碼的邏輯走下去, 你也許會發現這個看似無害的調用$valueRepository->findByValue() 最終執行了這樣一種查詢,例如:

        $result = $connection->query("SELECT `x`,`y` FROM `values` WHERE `value`=" . $inputValue);

        結果每輪循環都會產生一次對數據庫的查詢。 因此,假如你為這個循環提供了一個包含 1000 個值的數組,它會對資源產生 1000 單獨的請求!如果這樣的腳本在多個線程中被調用,他會有導致系統崩潰的潛在危險。

        因此,至關重要的是,當你的代碼要進行查詢時,應該盡可能的收集需要用到的值,然后在一個查詢中獲取所有結果。

        一個我們平時常常能見到查詢效率低下的地方 (例如:在循環中)是使用一個數組中的值 (比如說很多的 ID )向表發起請求。檢索每一個 ID 的所有的數據,代碼將會迭代這個數組,每個 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: 內存使用欺騙與低效

        一次取多條記錄肯定是比一條條的取高效,但是當我們使用 PHP 的 mysql 擴展的時候,這也可能成為一個導致 libmysqlclient 出現『內存不足』(out of memory)的條件。

        我們在一個測試盒里演示一下,該測試盒的環境是:有限的內存(512MB RAM),MySQL,和 php-cli

        我們將像下面這樣引導一個數據表:

        // 連接 mysql $connection = new mysqli('localhost', 'username', 'password', 'database');  // 創建 400 個字段 $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);  // 寫入 2 百萬行數據 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); }

        OK,現在讓我們一起來看一下內存使用情況:

        // 連接 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

        Cool。 看來就內存使用而言,內部安全地管理了這個查詢的內存。

        為了更加明確這一點,我們把限制提高一倍,使其達到 100,000。 額~如果真這么干了,我們將會得到如下結果:

        PHP Warning:  mysqli::query(): (HY000/2013):               Lost connection to MySQL server during query in /root/test.php on line 11

        究竟發生了啥?

        這就涉及到 PHP 的 mysql 模塊的工作方式的問題了。它其實只是個 libmysqlclient 的代理,專門負責干臟活累活。每查出一部分數據后,它就立即把數據放入內存中。由于這塊內存還沒被 PHP 管理,所以,當我們在查詢里增加限制的數量的時候, memory_get_peak_usage() 不會顯示任何增加的資源使用情況 。我們被『內存管理沒問題』這種自滿的思想所欺騙了,所以才會導致上面的演示出現那種問題。 老實說,我們的內存管理確實是有缺陷的,并且我們也會遇到如上所示的問題。

        如果使用 mysqlnd 模塊的話,你至少可以避免上面那種欺騙(盡管它自身并不會提升你的內存利用率)。 mysqlnd 被編譯成原生的 PHP 擴展,并且確實 使用 PHP 的內存管理器。

        因此,如果使用 mysqlnd 而不是 mysql,我們將會得到更真實的內存利用率的信息:

        Before: 232048 Limit 1: 324952 Limit 10000: 32572912

        順便一提,這比剛才更糟糕。根據 PHP 的文檔所說,mysql 使用 mysqlnd 兩倍的內存來存儲數據, 所以,原來使用 mysql 那個腳本真正使用的內存比這里顯示的

        贊(0)
        分享到: 更多 (0)
        網站地圖   滬ICP備18035694號-2    滬公網安備31011702889846號
        主站蜘蛛池模板: 日产国产精品亚洲系列| 国产精品揄拍100视频| 亚洲日韩精品无码专区网址| AAA级久久久精品无码区| 久久香蕉超碰97国产精品| 精品一区二区三区高清免费观看| 99香蕉国产精品偷在线观看| 亚洲无码日韩精品第一页| 国产呦小j女精品视频| 精品一区二区三区中文字幕| 人人妻人人澡人人爽人人精品97 | 国产精品一香蕉国产线看观看| 91在线视频精品| 久久久久久亚洲精品成人| 亚洲精品国产精品乱码不卞| 欧美精品在线免费| 精品欧美一区二区在线观看| 国产高清在线精品一区小说| 四虎影视国产精品亚洲精品hd| 国产精品欧美亚洲韩国日本不卡 | 日韩精品中文字幕第2页| 国产精品视频色视频| 99在线精品免费视频| 日本欧美韩国日本精品| 97精品国产91久久久久久| 99精品视频3| 国产精品九九九久久九九| 国产精品欧美亚洲韩国日本| 柠檬福利精品视频导航| 久久99国产精品久久| 91精品啪在线观看国产电影| 北条麻妃国产九九九精品视频| 亚洲欧美日韩精品永久在线| 国产亚洲精品国产| 久久综合九色综合精品| 亚洲国产精品婷婷久久| 99熟女精品视频一区二区三区| 国产一区二区精品久久岳| 久热精品人妻视频| 亚洲精品专区在线观看| 无码精品人妻一区二区三区人妻斩|