一、總結
該RFC建議新增4種新的標量型別宣告:int,float,string和bool,這些型別宣告將會和PHP原來的機制保持一致的用法。RFC更推薦給每一個PHP檔案,新增一句新的可選指令(declare(strict_type=1);),讓同一個PHP檔案內的全部函式呼叫和語句返回,都有一個“嚴格約束”的標量型別宣告檢查。此外,在開啟嚴格型別約束後,呼叫擴充或者PHP內建函式在引數解析失敗,將產生一個E_RECOVERABLE_ERROR級錯誤。通過這兩個特性,RFC希望編寫PHP能夠變得更準確和文件化。
二、細節
標量型別宣告:
沒有新增新的保留字。int、float、string和bool會被識別為型別宣告,同時禁止用作class/interface/trait等的命名。新的使用者標量型別宣告,通過內部的Fast Parameter Parsing API實現。
strict_types/declare()指令
預設情況下,所有的PHP檔案都處於弱型別校驗模式。新的declare指令,通過指定strict_types的值(1或者0),1表示嚴格型別校驗模式,作用於函式呼叫和返回語句;0表示弱型別校驗模式。
declare(strict_types=1)必須是檔案的第一個語句。如果這個語句出現在檔案的其他地方,將會產生一個編譯錯誤,塊模式是被明確禁止的。
類似於encoding指令,但不同於ticks指令,strict_types指令隻影響指定使用的檔案,不會影響被它包含(通過include等方式)進來的其他檔案。該指令在執行時編譯,不能修改。它的運作方式,是在opcode中設定一個標誌位,讓函式呼叫和返回型別檢查符合型別約束。
引數型別宣告
該指令影響全部的函式呼叫,例如(嚴格校驗模式):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?php declare(strict_types=1); foo(); // strictly type-checked function call function foobar() { foo(); // strictly type-checked function call } class baz { function foobar() { foo(); // strictly type-checked function call } } |
對比(弱校驗模式)
1 2 3 4 5 6 7 8 9 10 11 12 |
<?php foo(); // weakly type-checked function call function foobar() { foo(); // weakly type-checked function call } class baz { function foobar() { foo(); // weakly type-checked function call } } |
返回型別宣告:
指令會影響同一個檔案下的所有函式的返回型別. 例如(嚴格校驗模式):
1 2 3 4 5 6 7 8 9 10 11 12 |
<?php declare(strict_types=1); function foobar(): int { return 1.0; // strictly type-checked return } class baz { function foobar(): int { return 1.0; // strictly type-checked return } } |
1 2 3 4 5 6 7 8 9 10 11 |
<?php function foobar(): int { return 1.0; // weakly type-checked return } class baz { function foobar(): int { return 1.0; // weakly type-checked return } } |
弱型別校驗行為:
一個弱型別校驗的函式呼叫,和PHP7之前的PHP版本是一致的(包括擴充和PHP內建函式)。通常,弱型別校驗規則對於新的標量型別宣告的處理是相同的,但是,唯一的例外是對NULL的處理。為了和我們現有類、呼叫、陣列的型別宣告保持一致,NULL不是預設的,除非它作為一個引數並且被顯式賦值為NULL。
為了給不熟悉PHP現有的弱標量引數型別規則的讀者,提供簡短的總結。表格展示不同型別能夠接受和轉換的標量型別宣告,NULL、arrays和resource不能接受標量型別宣告,因此不在表格內。
*只有範圍在PHP_INT_MIN和PHP_INT_MAX內的non-NaN float型別可以接受。(PHP7新增,可檢視ZPP Failure on Overflow RFC)
?Non-numeric型字串不被接受,Numeric型字串跟隨字串的,也可以被接受,但是會產生一個notice。
?僅當它有__toString方法時可以。
嚴格型別校驗行為:
嚴格的型別校驗呼叫擴充或者PHP內建函式,會改變zend_parse_parameters的行為。特別注意,失敗的時候,它會產生E_RECOVERABLE_ERROR而不是E_WARNING。它遵循嚴格型別校驗規則,而不是傳統的弱型別校驗規則。嚴格型別校驗規則是非常直接的:只有當型別和指定型別宣告匹配,它才會接受,否則拒絕。
有一個例外的是,寬泛型別轉換是允許int變為float的,也就是說引數如果被宣告為float型別,但是它仍然可以接受int引數。
1 2 3 4 5 6 7 8 |
<?php declare(strict_types=1); function add(float $a, float $b): float { return $a + $b; } add(1, 2); // float(3) |
在這種場景下,我們傳遞一個int引數給到定義接受float的函式,這個引數將會被轉換為float。除此之外的轉換,都是不被允許的。
三、例子:
讓我們建立一個函式,讓2個數相加。
add.php
1 2 3 4 |
<?php function add(int $a, int $b): int { return $a + $b; } |
如果在分開的檔案,我們可以呼叫add函式通過弱型別的方式
1 2 3 4 5 6 7 8 9 |
<?php require "add.php"; var_dump(add(1, 2)); // int(3) // floats are truncated by default var_dump(add(1.5, 2.5)); // int(3) //strings convert if there's a number part var_dump(add("1", "2")); // int(3) |
預設情況下,弱型別宣告允許使用轉換,傳遞進去的值會被轉換。
1 2 3 4 5 |
<?php require "add.php"; var_dump(add("1 foo", "2")); // int(3) // Notice: A non well formed numeric value encountered |
但是,通過可選擇指令declare開啟嚴格型別校驗後,在這個場景下,相同的呼叫將會失敗。
1 2 3 4 5 6 7 8 9 |
<?php declare(strict_types=1); require "add.php"; var_dump(add(1, 2)); // int(3) var_dump(add(1.5, 2.5)); // int(3) // Catchable fatal error: Argument 1 passed to add() must be of the type integer, float given |
指令影響同一個檔案下的所有函式呼叫,不管這個被調函式是否在這個檔案內定義的,都會採用嚴格型別校驗模式。
1 2 3 4 5 |
<?php declare(strict_types=1); $foo = substr(52, 1); // Catchable fatal error: substr() expects parameter 1 to be string, integer given |
標量型別宣告也可以用於返回值的嚴格型別校驗:
1 2 3 4 5 6 7 |
<?php function foobar(): int { return 1.0; } var_dump(foobar()); // int(1) |
在弱型別模式下,float被轉為integer。
1 2 3 4 5 6 7 8 9 |
<?php declare(strict_types=1); function foobar(): int { return 1.0; } var_dump(foobar()); // Catchable fatal error: Return value of foobar() must be of the type integer, float returned |
四、背景和理論基礎
歷史
PHP從PHP5.0開始已經有對支援class和interface引數型別宣告,PHP5.1支援array以及PHP5.4支援callable。這些型別宣告讓PHP在執行的時候傳入正確的引數,讓函式簽名具有更多的資訊。
先前曾經想新增標量型別宣告,例如Scalar Type Hints with Casts RFC,因為各種原因失敗了:
(1)型別轉換和校驗機制,對於擴充和PHP內建函式不匹配。
(2)它遵循一個弱型別方法。
(3)它的“嚴格”弱型別修改嘗試,既沒有滿足嚴格型別的粉絲期望,也沒有滿足弱型別的粉絲。
這個RFC嘗試解決全部問題。
弱型別和強型別
在現代程式語言的實際應用中,有三種主要的方法去檢查引數和返回值的型別:
(1)全嚴格型別檢查(也就是不會有型別轉換髮生)。例如F#、GO、Haskell、Rust和Facebook的Hack的用法。
(2)廣泛原始型別檢查(“安全”的型別轉換會發生)。例如Java、D和Pascal。他們允許廣泛原始型別轉換(隱式轉換),也就是說,一個8-bit的integer可以根據函式引數需要,被隱形轉換為一個16-bit的integer,而且int也可以被轉換為float的浮點數。其他型別的隱式轉換則不被允許。
(3)弱型別檢查(允許所有型別轉換,可能會引起警告),它被有限制地使用在C、C#、C++和Visual Basic中。它們嘗試儘可能“不失敗”,完成一次轉換。
PHP在zend_parse_parameters的標量內部處理機制是採用了弱型別模式。PHP的物件處理機制採用了廣泛型別檢查方式,並不追求精確匹配和轉換。
每個方法各有其優缺點。
這個提案中,預設採用弱型別校驗機制,同時追加一個開關,允許轉換為廣泛型別校驗機制(也就是嚴格型別校驗機制)。
為什麼兩者都支援?
目前為止,大部分的標量型別宣告的擁護者都要求同時支援嚴格型別校驗和弱型別校驗,並非僅僅支援其中一種。這份RFC,使得弱型別校驗為預設行為,同時,新增一個可選的指令來使用嚴格型別校驗(同一個檔案中)。在這個選擇的背後,有很多個原因。
PHP社群很大一部分人看起來很喜歡全靜態型別。但是,新增嚴格型別校驗的標量型別宣告將會引起一些問題:
(1)引起明顯的不一致性:擴充和PHP內建函式對標量型別引數使用弱型別校驗,但是,使用者的PHP函式將會使用嚴格型別校驗。
(2)相當一部分人更喜歡弱型別校驗,並不贊同這個提案,他們可能會阻止它的實施。
(3)已經存在的程式碼使用了PHP的弱型別,它會受到影響。如果要求函式新增標量型別宣告到引數上,對於現有的程式碼庫,這將大大增加複雜性,特別是對於庫檔案。
這裡仍然有相當於一部分人是喜歡弱型別校驗的,但是,新增嚴格型別校驗宣告和新增弱型別校驗宣告都會引起一些問題:
(1)大部分傾向於嚴格型別校驗的人將不會喜歡這個提案,然後阻止它的實施。
(2)限制靜態解析的機會。(可能是說,優化的機會)
(3)它會隱藏一些在型別自動轉換中資料丟失的bug。
第三種方案被提出來了,就是新增區分弱型別和嚴格型別宣告的語法。它也會帶來一些問題:
(1)不喜歡弱型別和嚴格型別校驗的人,會被強迫分別處理被定義為嚴格型別或者弱型別校驗的庫。
(2)像新增嚴格宣告一樣,這個也將和原來弱型別實現的擴充和PHP內建函式無法保持一致。
為了解決這三種方案帶來的問題,這個RFC提出了第四種方案:每個檔案各自定義嚴格或者弱型別校驗。它帶來了以下好處:
(1)人們可以選擇適合他們的型別校驗,也就是說,這個方案希望同時滿足嚴格和弱型別校驗兩個陣營。
(2)API不會被強制適應某個型別宣告模式。
(3)因為檔案預設使用弱型別校驗方案,已經存在的程式碼庫,可以在不破壞程式碼結構的情況下,新增標量型別宣告。也可以讓程式碼庫逐步新增型別宣告,或者僅部分模組新增。
(4)只需要一個單一語法,就可以定義標量型別宣告。
(5)更喜歡嚴格型別校驗的人,通常,不僅將這個特性使用在使用者定義的函式,同時也使用在擴充和PHP內建函式中。也就是說,PHP使用者會得到一個統一機制,而不會產生嚴格標量宣告的矛盾。
(6)在嚴格型別校驗模式下,擴充和PHP內建函式產生的型別校驗失敗的錯誤級別,和使用者自定函式產生的會保持一致,都是E_RECOVERABLE_ERROR。
(7)它允許嚴格型別和弱型別程式碼,在一個單一的程式碼庫中無縫整合。
本文重點關注對PHP7標量型別宣告的介紹,因此,只翻譯了一部分英文原文,並非全文完整翻譯。敬請注意哈。