2016年11月8日 星期二

撰寫習慣、細節觀念

每種程式語言都有屬於它自己的特性,良好的撰寫習慣可能程式具有可閱讀性,幫助理解方便維護。能夠充分熟悉它的人在撰寫時越容易避免預期外的錯誤,以減少不必要的除錯動作,增加工作效率,因此此篇文章主要用來整理在學習 PHP 過程中應培養的撰寫習慣及應注意的細節觀念…等等的心得。

基本的撰寫習慣

  1. 檔案開頭以 <?php 做為 PHP 的起始標籤,盡量避免使用縮寫 <? 。
  2. 檔案若為單純的 PHP 程式碼,則應省略 PHP 的結尾標籤 ?> ,僅於結尾處加上註解表示檔案結尾。
    • ?> 結尾標籤在 PHP 編譯器中是非必要的。
    • 可避免他人在結尾標籤之後加上不可見的字元(空白、換行、TAB等等),可能會破壞頁面輸出的字元。
    • 本網誌的範例程式碼會使用 ?> 做為結尾符號,以保持程式碼一致性。
      <?php
          echo 'Hello World';
      
  3. 習慣為程式碼加上必要的註解。
    • 養成加上註解的習慣,不但能幫助自己記憶,也能讓別人更容易看懂程式碼的用途。
  4. 手動對變數做初始化的動作。
    • 在 PHP 中使用變數時,若該變數尚未被定義,PHP 編譯器會丟出 E_NOTICE 訊息,並自動替變數做初始化的動作。
    • 雖然 E_NOTICE 訊息並不會影響程式執行,卻容易使程式碼變得不嚴謹且產生模糊地帶。
    • 建議習慣手動對變數做初始化的動作,以增加程式嚴謹性。
      <?php
      $array = array();
      if (5 > 2) {
          $array[] = 5;
      } else {
          $array[] = 2
      }
      print_r($array);
      ?>
      
  5. 純字串使用單引號 (Single Quote) 為主。
    • 若字串僅為純文字,不需要經過轉換特殊字元的動作,建議使用單引號即可,避免 PHP 編譯器進行多餘的動作可增加效能。
    • 使用單引號僅會對字串進行少數幾個特殊字元轉換的動作,如 \'  \\ 。
    • 雙引號包覆的字串內容則會經過完整的轉換,例如變數內容置換、特殊字元轉換等操作,如\n(換行)、\(製表符)...。
    • 記住一個html的觀念,就是換行或是空白,是要透過<br /> <p> &nbsp;等等去輸出的你在原始碼內換行是沒有作用的,瀏覽器不會自動幫你顯示出來(原始碼的換行只是好看好整理用的)
      • 提供你兩個作法
        • <pre>的作用是原始碼排版即顯示結果
        • nl2br($output); 作用是幫你把換行字元轉成<br> 
    • <?php
          $name = 'Eden';
          echo 'Hello $name';     // $name 會被當作純文字,不做任何變數替換的動作
               echo 'Hello $name\n';   // \n 也是被當作純文字,不會被轉換成換行字元
               echo "Hello $name";     // $name 會被替換為 Eden
               echo "Hello $name\n";   // \n 會被轉換成換行字元
            echo "Hello $name", PHP_EOL;  // 換行字元建議改用 PHP_EOL 來輸出
      ?>
      
  6. 使用陣列時,其陣列索引應盡量以單引號或雙引號括起來。
    • 若以嚴謹標準來看 $array[ID] 的寫法是錯誤的,其陣列索引應該要被引號括起來為 $array['ID']較為正確。
    • PHP 編譯器會將沒有括起來的字串索引當作「祼字符」,先將它解釋成 Constant 後找不到其定義時,才再將它重新解釋成字串,因此 $array[ID] 是在這項特性之下被正常執行的。
      • 這項特性所提供的彈性是用執行效能換來的 : $array['ID'] 的執行速度是 $array[ID] 的七倍。
      • 即使陣列索引本身是數值,其 $array[0] 與 `$array['0'] 都是指向同一個陣列元素。
    • 因此建議在非必要的情況下,使用陣列時務必將索引括起來。
      <?php
      $prices = array('Milk' => 30, 'Tea' => 15, 'Coffee' = 35);
      echo $prices['Milk'];      # 索引以單引號括住
           // 使用 Constant 存取陣列 
           define('EDEN', 'Tea');     # 將 EDEN 定義為祼字符
           echo $prices[EDEN];
      // 使用 Variable 存取陣列
           $coffee = 'Coffee';
      echo $prices[$coffee];
      ?>
      
  7. 使用外來變數時,先利用 isset() 或 array_key_exists() 等相關機制來檢查變數是否存在。
    • 直接使用未被定義的變數時,PHP 會丟出 E_NOTICE 訊息。
      • E_NOTICE 訊息不影響程式執行,且可透過設定 ERROR_REPORTING | DISPLAY_ERROR 的機制來忽略這類訊息。
    • 但為避免非預期性的錯誤,建議在使用 $GET, $POST, $REQUEST 等外來變數,或無法確保變數是否已存在的情況時,記得先檢查該變數是否存在。
      <?php
      /* 不夠嚴謹的寫法 */
      // 1. 當 $_POST 不存在或送出的表單沒有 password/account 欄位時,PHP 會丟出 ENOTICE (undefined index)
           if ($_POST['password'] == 'PASSWORD') {
          $admin = $POST['account'];
      }
      // 2. 當上述 if 不成立時,$admin 就不會被定義,因此 PHP 會丟出 E_NOTICE (undefined variable) 
           echo $admin . PHP_EOL;
      /* 較嚴謹且正確的寫法:變數先給預設值,且使用外來變數前先檢查是否存在 */
      $admin = null;
      if (isset($POST['password']) && ($POST['password'] == 'PASSWORD')) {
          $admin = (isset($_POST['account'])) ? $_POST['account'] : $admin;
      }
      echo $admin . PHPEOL;
      // 除了利用 isset() 以外,也能透過 array_key_exists() 來檢查。
           if (array_key_exists('password', $_POST)) {
          // Do something here.
           }
      ?>
      
  8. 在使用 ==!====!== 比較運算子時,必須多加注意以避免預期外的錯誤。
    • 例如比較 Boolean 布林值時應盡量使用 ===!== 比較運算子以增加程式嚴謹度。
    • 基於 PHP 自動轉換變數型態的特性,因此需注意比較運算子的差異。
      • ==!= 比較的只有變數內容,當型態不同時會自動轉換成相同型態。
      • ===!== 除變數內容以外,還會比較變數的型態。
      • 更多詳細內容請參考官方手冊: PHP type comparison tables

增加程式可讀性的撰寫習慣

  1. 找一個多數人認同的程式碼撰寫規範來遵守。
    • 遵守一個多數人認同的程式碼規範,不僅可提高程式碼的可讀性,亦能讓團隊成員共同撰寫出具有相同規則的程式碼。
    • PSR Coding Style for PHP (待補充)

優化效能的撰寫習慣

  1. 使用迴圈時,非必要應避免在迴圈條件式中直接使用函數。
    • 若函式被寫在迴圈條件式中,在每次迴圈檢查條件式時都會執行該函式一次。
    • 若函式回傳值在迴圈的過程中不會改變,則應避免將函式寫在迴圈條件式中。 (反覆執行函式卻取得同樣的結果,只是浪費系統資源及降低程式效能。)
      <?php
      $array = range(0, 100000);  # 產生陣列
           // 不適當的寫法,因為 count($array) 反覆執行 N 次卻都是取得同樣的結果
           for ($i = 0; $i < count($array); $i++) {
          echo $array($i);
      }
      // 較適當的寫法,count($array) 僅會執行一次
           $arrayCount = count($array);
      for ($i = 0; $i < $arrayCount; $i++) {
          echo $array($i);
      }
      // [例外狀況] 將陣列元素增加至 11000 個 : count($array) 的回傳值會因迴圈過程中的動作而被改變
           for ($i = 0; count($array) < 11000; $i++) {
          $array[] = $i; 
      }
      ?>
      
  2. 盡量避免使用正規表達式 (Regular Expression) 。
    • 在進行字串操作時,非必要應盡量避免使用正規表達式。
      • 例如 str_replace() 的執行效率會比 preg_replace() 快,
    • 在必要時仍以使用正規表達式達到簡化工作,容易維護為主。
      • 例如檢查 Email 格式若以其他方式來完成,除了得費一番功夫以外,也不易維護。
  3. 使用完變數之後,必要時手動釋放變數資源。
    • 當變數被用以儲存龐大資料時,建應於使用完畢後立即使用 unset 以釋放該變數所佔有的資源。
      <?php
          $article = '假設這是一篇文字超長,佔記憶體至少 100MB 的文章...';
          // Do something...以下數百行(略)
               unset($article);  // 釋放變數資源
               // Do something else 以下數百行(略)
      ?>
      

其他程式相關的細節

  1. 陣列宣告時,最後一個元素之後可以有多餘的逗點。
    • 雖然有彈性但不建議用,因為在程式撰寫時還是保有一定的嚴謹度,以減少不可預期的錯誤。
      <?php
          // 多數程式語言的寫法,最後一個元素之後有不能多餘的逗點
               $array = array(1,2,3);
          // 由於 PHP 本身的彈性,最後一個元素之後可接受多餘的逗點
               $array = array(1,2,3,);   // 不會出現錯誤。
      ?>
      
  2. 陣列 $array[0] 與 $array['0'] 是指向同一個陣列元素。
    • 由於 PHP 具有自動轉換變數型態的特性,透過變數操作陣列時就產生了一個疑問:
      • 若 $index = '5' 時, $array[$index] 究竟是 $array['5'] 還是 $array[5]?
    • 因此就撰寫了簡單的測試碼,得出以下結果:
      <?php
          $array = array(
              0   => 'Number : 0',
              '0' => 'String : Zero'
          );
          print_r($array);  // 輸出結果只有 [0] => 'String : Zero'
      ?>
      
  3. 使用函式傳遞參數時,若傳遞的參數是物件時為 Call-Time Pass By Reference 。
    • 陣列及一般變數為 Pass By Value 的方式,可在函式定義時加上 & 符號強制改以 Pass By Reference 傳入。
      • PHP 5.3 時對 Call-Time Pass By Reference 做了些改變,當有 Pass By Reference 的需求時,必須在定義函式時明確指定該引數的傳入方式,且不可在呼叫函式時使用 & 符號。
    • 請參考以下範例:
      <?php
      // 定義一個簡單的類別
           Class MyObject 
      {
          public $name = null;
          public function setName($name)
          {
              $this->name = $name; 
          }
      }
      
      // 定義測試用的一般函式
           function demoBasics($object, $array, $var)
      {
          $object->setName('Basics');
          $array = array('Basics');
          $var = 'Basics';
      }
      // 定義測試用的函式 (引數前加上 & 表示強制以 Pass By Reference 方式傳入)
           function demoReference(&$object, &$array, &$var)
      {
          $object->setName('Reference');
          $array = array('Reference');
          $var = 'Reference';
      }
      // 測試範例
           $myObject = new MyObject();
      $myObject->setName('Default');
      $myArray = array('Default');
      $myVar = 'Default';
      // 正常情況下為 Pass By Value 傳遞參數
        demoBasics($myObject, $myArray, $myVar);
      print_r($myObject);   // 輸出 name => 'Basics',以 Pass by Reference 傳遞物件參數
           print_r($myArray);    // 輸出 [0] => 'Default'
           print_r($myVar);      // 輸出 'Default';
        // 強制以 Pass By Reference 傳遞參數
           demoReference($myObject, $myArray, $myVar);
      print_r($myObject);   // 輸出 name => 'Reference'
           print_r($myArray);    // 輸出 [0] => 'Reference'
           print_r($myVar);      // 輸出 'Reference';
      
           // 註:下列這行程式碼在 PHP 5.3 以上版本時無法通過編譯
           // 必須在定義函式時就明確指定變數以 Pass By Reference 的方式傳入。
           demoReference(&$myObject, &$myArray, &$myVar);
      ?>

沒有留言: