令人期待的 PHP7.4

lxping發表於2019-08-28

PHP 7.4 是下一個 PHP 7 的次要版本,預計將於 2019 年 11 月 28 日釋出到 General Availability。讓我們來了解下 PHP 7.4 新增的功能,這些新功能將使 PHP 更快,更可靠。

當然,更令我期待是 PHP 8。因為 JIT 的一些提議,已經獲得批准,這可能成為 PHP 的又一個里程碑。

PHP 7.4 有什麼新功能?

  • 支援陣列內解包 - 陣列擴充套件運算子
  • 箭頭函式 2.0 (更加簡短的閉包)
  • NULL 合併運算子
  • 弱引用
  • 協變返回和逆變引數
  • 預載入

陣列表示式中引入 Spread 運算子

自PHP 5.6起可用,引數解包是將陣列和Traversable解包為引數列表的語法。要解壓一個陣列或Traversable,必須以 ...(3點)為字首,如下例所示:

  function test(...$args) { var_dump($args); }
  test(1, 2, 3);

然而PHP 7.4 RFC建議將此功能擴充套件到陣列中去定義:

  $arr = [...$args];

Spread 運算子的第一個好處就是效能,RPC 文件指出:

Spread 運算子應該比 array_merge 擁有更好的效能。這不僅僅是 Spread 運算子是一個語法結構,而 array_merge 是一個方法。還是在編譯時,優化了高效率的常量陣列

Spread運算子的一個顯著優點是它支援任何可遍歷的物件,而該array_merge函式僅支援陣列。

以下是陣列中引數帶有 Spread 運算子的示例:

  $parts = ['apple', 'pear'];
  $fruits = ['banana', 'orange', ...$parts, 'watermelon'];
  var_dump($fruits);

如果在PHP 7.3或更早版本中執行此程式碼,PHP會丟擲一個Parse錯誤:

  Parse error: syntax error, unexpected '...' (T_ELLIPSIS), expecting ']' in /app/spread-operator.php on line 3

相反,PHP 7.4將返回一個陣列

  array(5) {
    [0]=>
    string(6) "banana"
    [1]=>
    string(6) "orange"
    [2]=>
    string(5) "apple"
    [3]=>
    string(4) "pear"
    [4]=>
    string(10) "watermelon"
  }

RFC宣告我們可以多次擴充套件同一個陣列。此外,我們可以在陣列中的任何位置使用Spread Operator語法,因為可以在spread運算子之前或之後新增常規元素。因此,以下程式碼將按預期工作:

  $arr1 = [1, 2, 3];
  $arr2 = [4, 5, 6];
  $arr3 = [...$arr1, ...$arr2];
  $arr4 = [...$arr1, ...$arr3, 7, 8, 9];

也可以將函式返回的陣列作為引數,放到新陣列中:

  function buildArray(){
    return ['red', 'green', 'blue'];
  }
  $arr1 = [...buildArray(), 'pink', 'violet', 'yellow'];

PHP 7.4輸出以下陣列:

  array(6) {
    [0]=>
    string(3) "red"
    [1]=>
    string(5) "green"
    [2]=>
    string(4) "blue"
    [3]=>
    string(4) "pink"
    [4]=>
    string(6) "violet"
    [5]=>
    string(6) "yellow"
  }

我們也可以使用生成器

  function generator() {
    for ($i = 3; $i <= 5; $i++) {
        yield $i;
    }
  }
  $arr1 = [0, 1, 2, ...generator()];

但不允許通過引用傳遞的方式。請考慮以下示例:

  $arr1 = ['red', 'green', 'blue'];
  $arr2 = [...&$arr1];

如果我們嘗試通過傳遞引用的方式,PHP會丟擲以下Parse錯誤:

  Parse error: syntax error, unexpected '&' in /app/spread-operator.php on line 3

如果第一個陣列的元素是通過引用儲存的,那麼它們也通過引用儲存在第二個陣列中。這是一個例子:

  $arr0 = 'red';
  $arr1 = [&$arr0, 'green', 'blue'];
  $arr2 = ['white', ...$arr1, 'black'];

這是我們用PHP 7.4獲得的:

  array(5) {
    [0]=>
    string(5) "white"
    [1]=>
    &string(3) "red"
    [2]=>
    string(5) "green"
    [3]=>
    string(4) "blue"
    [4]=>
    string(5) "black"
  }

箭頭函式 2.0 (簡短閉包)

在 PHP 中,匿名函式被認為是非常冗長且難以實現和難以維護的,RFC建議引入更簡單,更清晰的箭頭函式(或簡短閉包)語法,這樣我們就可以簡潔地編寫程式碼。

在 PHP 7.4 以前:

  function cube($n){
    return ($n * $n * $n);
  }
  $a = [1, 2, 3, 4, 5];
  $b = array_map('cube', $a);
  print_r($b);

PHP 7.4允許使用更簡潔的語法,上面的函式可以重寫如下:

  $a = [1, 2, 3, 4, 5];
  $b = array_map(fn($n) => $n * $n * $n, $a);
  print_r($b);

目前,由於語言結構,匿名函式(閉包)可以使用 use 繼承父作用域中定義的變數,如下所示:

  $factor = 10;
  $calc = function($num) use($factor){
    return $num * $factor;
  };

但是在PHP 7.4中,父級作用域的值是通過隱式捕獲的(隱式按值的作用域進行繫結)。所以我們可以用一行來完成一下這個函式:

  $factor = 10;
  $calc = fn($num) => $num * $factor;

父級作用域定義的變數可以用於箭頭函式,它跟我們使用 use 是等價的,並且不可能被父級所修改。

新語法是對語言的一個很大改進,因為它允許我們構建更易讀和可維護的程式碼。

NULL 合併運算子

由於日常使用中存在大量同時使用三元表示式和 isset()的情況, 我們新增了null合併運算子 (??) 這個語法糖。如果變數存在且值不為NULL, 它就會返回自身的值,否則返回它的第二個運算元。

  $username = $_GET['user'] ?? ‘nobody';

這段程式碼的作用非常簡單:它獲取請求引數並設定預設值(如果它不存在)。但是在 RFC 這個例子中,如果我們有更長的變數名稱呢?

  $this->request->data['comments']['user_id'] = $this->request->data['comments']['user_id'] ?? 'value';

長遠來看,這段程式碼可能難以維護。因此,旨在幫助開發人員編寫更直觀的程式碼,這個 RFC 建議引入 null合併等於運算子(null_coalesce_equal_operator)??=,所以我們可以敲下面這段程式碼來替代上面的這段程式碼:

  $this->request->data['comments']['user_id'] ??= ‘value’;

如果左側引數的值為null,則使用右側引數的值。

注意,雖然 coalesce運算子??是一個比較運算子,但??=它是賦值運算子。

型別屬性 2.0

型別的宣告,型別提示,以及指定確定型別的變數傳遞給函式或類的方法。其中型別提示是在 PHP5 的時候有的一個功能,PHP 7.2 的時候新增了 object 的資料型別。而 PHP7.4 更是增加了主類屬性宣告,看下面的例子:

  class User {
    public int $id;
    public string $name;
  }

除了 voidcallable 外,所有的型別都支援:

  public int $scalarType;
  protected ClassName $classType;
  private ?ClassName $nullableClassType;

為什麼不支援 voidcallable?下面是 RFC 的解釋

The void type is not supported, because it is not useful and has unclear semantics.
不支援void型別,是因為它沒用,並且語義不清晰。

The callable type is not supported, because its behavior is context dependent.
不支援callable型別,因為其行為取決於上下文。

因此,我們可以放心使用 boolintfloatstringarrayobjectiterableselfparent,當然還有我們很少使用的 nullable 空允許 (?type)

所以你可以在 PHP7.4 中這樣敲程式碼:

  // 靜態屬性的型別
  public static iterable $staticProp;

  // var 中宣告屬性
  var bool $flagl

  // 設定預設的值
  // 注意,只有 nullable 的型別,才能設定預設值為 null
  public string $str = "foo";
  public ?string $nullableStr = null;

  // 多個同型別變數的宣告
  public float $x, $y;

如果我們傳遞不符合給定型別的變數,會發生什麼?

  class User {
    public int $id;
    public string $name;
  }

  $user = new User;
  $user->id = 10;
  $user->name = [];

  // 這個會產生一個致命的錯誤
  Fatal error: Uncaught TypeError: Typed property User::$name must be string, array used in /app/types.php:9

弱引用

在這個 RFC 中,提議引入WeakReference 這個類,弱引用允許編碼時保留對物件的引用,該引用不會阻止物件被破壞; 這對於實現類似於快取的結構非常有用。

該提案的作者 Nikita Popov 給出的一個例子:

  $object = new stdClass;
  $weakRef = WeakReference::create($object);

  var_dump($weakRef->get());
  unset($object);
  var_dump($weakRef->get());

  // 第一次 var_dump
  object(stdClass)#1 (0) {}

  // 第二次 var_dump,當 object 被銷燬的時候,並不會丟擲致命錯誤
  NULL

協變返回和逆變引數

協變和逆變
百度百科的解釋

  • Invariant(不變): 包好了所有需求型別
  • Covariant(協變):型別從通用到具體
  • Contravariant(逆變): 型別從具體到通用

    目前,PHP主要具有Invariant的引數型別,並且大多數是Invariant的返回型別,這就意味著當我是 T 引數型別或者返回型別時,子類也必須是 T 的引數型別或者返回型別。但是往往會需要處理一些特殊情況,比如具體的返回型別,或者通用的輸入型別。

    RFC的這個提案就提議,PHP7.4 新增協變返回和逆變引數,以下是提案給出來的例子:

    協變返回:

    interface Factory {
      function make(): object;
    }
    
    class UserFactory implements Factory {
      // 將比較泛的 object 型別,具體到 User 型別
     function make(): User;
    }

    逆變引數:

    interface Concatable {
      function concat(Iterator $input); 
    }
    
    class Collection implements Concatable {
      // 將比較具體的 `Iterator`引數型別,逆變成接受所有的 `iterable`型別
      function concat(iterable $input) {/* . . . */}
    }

    預載入

    這個RFC 是由 Dmitry Stogov 提出的,預載入是在模組初始化的時候,將庫和框架載入到OPCache中的過程,如下圖所示

    PHP生命週期(圖片來源:PHP Internals)

    引用他的原話:

    On server startup – before any application code is run – we may load a certain set of PHP files into memory – and make their contents “permanently available” to all subsequent requests that will be served by that server. All the functions and classes defined in these files will be available to requests out of the box, exactly like internal entities.

    伺服器啟動時 - 在執行任何應用程式程式碼之前 - 我們可以將一組PHP檔案載入到記憶體中 - 並使得這些預載入的內容,在後續的所有請求中“永久可用”。這些檔案中定義的所有函式和類在請求時,就可以開箱即用,與內建函式相同。

    預載入由 php.iniopcache.preload 進行控制。這個引數指定在伺服器啟動時編譯和執行的PHP指令碼。此檔案可用於預載入其他檔案,或通過opcache_compile_file()函式

    這在效能上有很大的提升,但是也有一個很明顯的缺點,RFC 提出來了

    preloaded files remain cached in opcache memory forever. Modification of their corresponding source files won’t have any effect without another server restart.

    預載入的檔案會被永久快取在 opcache 記憶體中。在修改相應的原始檔時,如果沒有重啟服務,修改就不會生效。

PHP7.4 又將廢棄什麼功能呢?

  • 更改連線運算子的優先順序

更改連線運算子的優先順序

目前,在PHP中 + , - 算術運算子和 . 字串運算子是左關聯的, 而且它們具有相同的優先順序。例如:

  echo "sum: " . $a + $b;

在PHP 7.3 中,此程式碼生成以下警告:

  Warning: A non-numeric value encountered in /app/types.php on line 4

這是因為這段程式碼是從左往右開始的,所以等同於:

  echo ("$sum: " . $a) + $b;

針對這個問題,這個RFC建議更改運算子的優先順序,使 . 的優先順序低於 +- 這兩個運算子,以便在字串拼接之前始終執行加減法。所以這行程式碼應該等同於以下內容:

  echo "$sum: " . ($a + $b);

這個提案分為兩步走:

  • 從 PHP7.4 開始,當遇見 + -. 在沒有指明執行優先順序時,會發出一個棄用通知。
  • 而真正調整優先順序的這個功能,會在 PHP8 中執行

    棄用左關聯三元運算子

    在PHP中,三元運算子與許多其他語言不同,它是左關聯的。而根據Nikita Popof的所說:對於在不同語言之間切換的程式設計人員來說,會令他們感到困擾。

    比如以下的例子,在 PHP 中是正確的:

    $b = $a == 1 ? 'one' : $a == 2 ? 'two' : $a == 3 ? 'three' : 'other';

    它會被解釋為:

    $b = (($a == 1 ? 'one' : $a == 2) ? 'two' : $a == 3) ? 'three' : 'other';

    對於這種複雜的三元表現形式,它很有可能不是我們希望的方式去工作,容易造成錯誤。因此,這個RFC 提議刪除並棄用三元運算子的左關聯使用,強制程式設計人員使用括號。

    這個提議分為兩步執行:

  • 從 PHP7.4 開始,沒有明確使用括號的巢狀三元組將丟擲棄用警告。
  • 從PHP 8.0開始,將出現編譯執行時錯誤。

相關文章