天天看點

《Hack與HHVM權威指南》——2.7 進階:協變和逆變

本節書摘來自華章出版社《hack與hhvm權威指南》一書中的第2章,第2.7節,作者 owen yamauchi,更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視。

除非你正在編寫一些很常見的,類似集合(collection)的代碼庫,否則你可能并不需要閱讀下面的内容。對于日常上大多數用例來說,你隻需要知曉我們正在讨論的規則是存在的,并且知道為什麼。本小節的内容是關于“當你必須要修改相關規則的時候将如何做”。

關于泛型的亞型關系是如何被它們類型實參的亞型關系影響的概念,我們稱之為“變體(variance)”。這裡三種不同的變體。設想一下,我們有個泛型類叫做thing,這個類有個類型形參t。然後(我們使用int和num作為類型實參的示例):

如果thing是thing的亞型,我們說thing對于t是協變的(covariant)。數組在它所有的類型形參上都是協變的,不可變集合類在它們值的類型形參上也都是協變的。

如果thing是thing的亞型,我們說thing在t上是逆變的(contravariant)。這違反直覺,但這也是可能的,這裡就有逆變類型的真實程式。

如果上面兩項都不為真,那麼我們說thing在t上是不變的。

為了使一個泛型在它的類型形參上是協變,就放置一個加号在類型形參之前。請僅在參數清單上做這件事情。在定義内部,還和以前一樣使用類型形參的名字。類似地,如果想使一個泛型在它的類型形參上是逆變的,那麼請放置一個減号在類型形參之前。如下所示:

一個類可以有不同的變量類型形參:

下面有一些幫助你記憶相關術語和文法的幾個知識點:

協變(covariance)

單詞的字首“co-”意味着“協同(with)”,對于一個協變類型形參來說,泛型的亞型關系和實參的亞型關系“在同一個方向上”變化,因為它們沿相同的方向變化,是以對應的符号是個加号。

逆變(contravariance)

單詞的字首“contra-”意味着“對抗(against)”,對于一個逆變類型的形參來說,泛型的亞型關系和實參的亞型關系相反。因為它們沿相反的方向變化,是以符号是個減号。

對于你寫下的大多數類來說,并不會用到協變或者逆變。這個特性僅在一些特殊情況下有作用:

協變主要用于隻讀類型。比如,如果我們從類wrapper中移除方法setvalue(),那麼對于它的類型形參tval來說,它就是隻讀的。也就是說,它隻輸出類型tval的值;除了在構造函數中,它從不使用它們作輸入項目。是以在tval上,wrapper能夠是協變的注4。

逆變是用來隻寫類型的。舉例來說,一個序列化類型t的值到日志檔案的泛型類,對于類型t的值可能是隻寫的。更确切地說,它隻把類型t的值為輸入,但是從來不輸出它們。

類型檢查器通過在怎樣使用協變和逆變類型形參上設定限制條件來執行它。明确地說,每種類型形參隻允許出現在代碼上的特定位置,它們被稱為協變位置和逆變位置。

首先,我們介紹簡單的部分:

public和protected的屬性類型僅被限制為不可變類型形參。

傳回類型被限制為不可變或者協變的類型形參。它們是協變位置。

除構造函數外,函數和方法的形參類型被限制為不可變或者逆變的類型形參。它們是逆變位置。

私有屬性類型和構造函數形參類型沒有類型形參方面的限制。

然後,我們說明稍微複雜的部分。在另外一個逆變位置内部,還可以有另外一個逆變位置。而内部逆變位置事實上是協變的。請看下面的示例:

逆變類型形參t出現在一個形參類型($callback的類型)上,而後一個參數類型又出現在了另外一個形參類型(passtocallback()的類型) 的内部。這就是在一個逆變位置内部的另外一個逆變位置。是以它是協變的。是以它是非法的。

你可以具體看看為什麼會是這樣,直覺上,passtocallback()的寫法将導緻對于writeonly外部的内容,在writeonly執行個體之外出現得到類型t值的可能性。

協變位置内部的協變位置仍然是協變的。協變和逆變的工作就像乘法下的正數和負數,正數乘以正數得到正數,但是負數乘以負數得到的也是正數。

協變

讓我們在類wrapper中移除方法setvalue(),然後把類型形參設定為協變的:

協變類型形參tval以如下情形出現:一個私有屬性的類型、一個構造函數的參數、一個傳回類型。這些都是協變類型形參允許出現的地方。類型檢查器将會接受這些代碼而不會報錯。

接下來的代碼現在也是被接受的。當我們把wrapper按照wrapper來對待的時候,放置在協變類型形參上的限制條件保證了這裡沒有什麼途徑可以打破類型安全限制。

如果你試圖添加一個改變值的方法,類型檢查器将會報告一個錯誤:一個協變類型形參出現在一個不可協變的位置上。

同樣的道理,如果你改變$value屬性的通路修飾符為public或者protected,類型檢查器也會報告一個錯誤:一個非私有的屬性總是一個不變的位置。也就是說,你不能在這裡使用協變或者逆變的類型形參。

逆變

逆變類型是比較少見的,這是因為隻寫的類型比隻讀的類型罕見。我們接下來将會通過一個類來加深對逆變的了解。在這個類中,我們建立了一個值的

請注意,逆變類型形參tval隻出現在一個方法的參數和一個私有屬性之中,是以類型檢查器接受了這段代碼。如果你試圖把$buffer設定為public或者protected,或者為這個類添加一個傳回類型為tval的方法,類型檢查器都會報告一個錯誤。

這個違反直覺的逆變類型形參意味着jsonlogger是jsonlogger的一個亞型。由下面的代碼來進行驗證:

這裡的代碼把jsonlogger傳遞給了一個期待參數為jsonlogger的函數。這是可以的,因為jsonlogger可以做任何jsonlogger可以做的事情(甚至更多)。因為這裡在jsonlogger之外,沒有任何途徑可以擷取tval類型的值。在這個類外部的代碼也不能從它擷取其不期待的類型的值。