2018年11月25日 星期日

處處訝異的怪怪語言 php

最近用 php 寫了一個小程式 jesp 可以拿來把好幾個 csv 檔 join 在一起, 並且讓訪客可以很方便地按照任何欄位排序。 過程當中發現 php 語言有很多怪怪的地方, 包含表達能力不足的設定檔、 用過一次就會有意外副作用的 reference 變數、 不能疊起來的三元運算子。 這真是一個處處充滿詫異的神奇語言啊~~

一、 設定檔

你的 php 程式應該以什麼方式讀設定檔? 最多人推的解答是採用 include。 但我希望使用者可以在網址列上用 query string 指定設定檔, 而 以這種方式使用 include 會開啟重大安全漏洞 -- 例如你的伺服器上的其他用戶可以藉此以你的身份執行他寫的程式。 第二名的解答是採用 parse_ini_file 。 可是 ini 檔案格式的表達力有限, 無法表達多層的陣列。 所以最後決定採用 json 格式。 注意: 讀取內含中文的 json 檔時, 要加上第二個參數 「TRUE」, 類似這樣: $config = json_decode(file_get_contents($config), TRUE);

二、 foreach 迴圈的 reference dummy variable

php 的 foreach 迴圈允許你用 & 符號表達 reference, 也就是允許你在迴圈當中修改陣列的元素。 那麼你覺得這段程式碼會印出什麼呢?

<?php
$L = array(18, 25, 3);
foreach ($L as &$x) { $x += 1; }
foreach ($L as $x) { echo " $x"; }
echo "\n";
?>

答案是 19 26 26 大驚! 原來是因為經過第一個迴圈之後, $x 跟 $L[2] 已經合體變成同一個變數, 所以在第二次的迴圈當中, 那個 $x 其實是... 也就是說 $L[2] 被拿來成迴圈變數, 導致它的值逐次變成陣列當中其他每個元素的值, 最後變成倒數第二個元素的值。 詳見 php foreach pass by reference bug? wierd behavior of foreach 我看還是不要用 reference 版的 foreach 迴圈比較簡單。

三、 三元運算子

在其他語言用過三元運算子吧? printf("%s\n", 5>3 ? "A" : "B"); 會印出 A。 但若把兩個三元運算子疊起來呢? (nested ternary operator) C 語言的版本會印出 "B", 一如預料:

#include <stdio.h>
int main() {
    printf("%s\n", 5>3 ? "A" : 5<3 ? "B" : "C");
}

但同樣的程式碼, 在 php 的版本卻會印出 "A"!

<?php
    printf("%s\n", 5>3 ? "A" : 5<3 ? "B" : "C");
?>

原來是因為 php 的三元運算子的運算順序跟其他語言 (比較符合直覺的順序) 不一樣。 詳見 這個問答。 總之, 在 php 裡使用疊起來的三元運算子時, 一定要用括弧, 或乾脆避免疊起來最簡單。

四、 array_filter 的傳回值

以下程式碼會印出什麼?

$numbers = array(18, 25, 3, 49, 7);
$small = array_filter($numbers, fn($x)=>$x<10);
print("$mall[0]\n");

會印出 3 嗎? 不, 會出現錯誤訊息: Undefined offset: 0 用 print_r 查看完整的 $small 就會知道: php 的陣列,即使採用整數作為註標, 它的行為仍舊比較像是字串型態的 key, 中間可以跳號的。 所以上例當中, $small[2] 才是 3。 array_filter 傳回來的經常就是含有很多跳號的子陣列。 如果 想要得到「註標從0開始,沒有跳號」的陣列, 就要這樣做: array_values(array_filter(...))

1 則留言:

  1. php 作者前些日子還說過 php 當初就是拿來當樣板語言。
    結果現在還出現一堆 php 寫出來的樣板語言框架。(苦笑)
    php 語言本身進化過程加入許多新語言的特性過程中也埋下不少地雷。

    回覆刪除

因為垃圾留言太多,現在改為審核後才發佈,請耐心等候一兩天。