Groovy 語法類型知識(shí)詳解最終篇
1. 介紹
本篇是Typing相關(guān)知識(shí)的最后一篇。介紹關(guān)于類型的閉包和類型推斷關(guān)系,以及最終的類型靜態(tài)編譯相關(guān)知識(shí)點(diǎn)。
2. 閉包和類型推斷
類型檢查器對(duì)閉包執(zhí)行特殊的推斷,在一邊執(zhí)行額外的檢查,在另一邊提高流暢性。
2.1 返回類型推斷
類型檢查器能夠做的第一件事是推斷閉包的返回類型。下面的例子簡(jiǎn)單地說明了這一點(diǎn):
正如上面所看到的,與顯式聲明其返回類型的方法不同,不需要聲明閉包的返回類型:它的類型是從閉包的主體推斷出來的。
2.2 閉包vs方法
返回類型推斷僅適用于閉包。雖然類型檢查器可以對(duì)方法執(zhí)行相同的操作,但實(shí)際上并不可取:通常情況下,方法可以被覆蓋,并且靜態(tài)地不可能確保所調(diào)用的方法不是被覆蓋的版本。所以流類型實(shí)際上會(huì)認(rèn)為一個(gè)方法返回一些東西,而在現(xiàn)實(shí)中,它可以返回其他東西,如下面的例子所示:
通過上面的示例可以知道,如果類型檢查器依賴于方法的推斷返回類型(使用流類型),則類型檢查器可以確定是否可以調(diào)用toUpperCase?。這實(shí)際上是一個(gè)錯(cuò)誤,因?yàn)樽宇惪梢灾貙慶ompute?并返回不同的對(duì)象。這里,B.compute?返回一個(gè)整型,因此在B?的實(shí)例上調(diào)用computeFully將會(huì)看到一個(gè)運(yùn)行時(shí)錯(cuò)誤。編譯器通過使用方法的聲明返回類型而不是推斷返回類型來防止這種情況發(fā)生。
為了保持一致性,這種行為對(duì)于每個(gè)方法都是相同的,即使它們是靜態(tài)的或最終的。
2.3 參數(shù)類型推斷
除了返回類型外,閉包還可以從上下文推斷其參數(shù)類型。編譯器有兩種方法來推斷形參類型:
- 通過隱式SAM類型強(qiáng)制
- 通過API元數(shù)據(jù)
讓我們從一個(gè)由于類型檢查器無法推斷形參類型而導(dǎo)致編譯失敗的示例開始:
在這個(gè)例子中,閉包體包含了it.age?。對(duì)于動(dòng)態(tài)的、非類型檢查的代碼,這是可行的,因?yàn)樗念愋驮谶\(yùn)行時(shí)是Person?。不幸的是,在編譯時(shí),沒有辦法知道它的類型,只能通過讀取inviteIf的簽名。
2.3.1 顯式閉包參數(shù)
簡(jiǎn)而言之,類型檢查器在inviteIf?方法上沒有足夠的上下文信息來靜態(tài)確定it的類型。這意味著方法調(diào)用需要像這樣重寫:
通過顯式聲明it變量的類型,可以解決這個(gè)問題,并使此代碼進(jìn)行靜態(tài)檢查。
2.3.2 從單一抽象方法類型推斷出的參數(shù)
對(duì)于API或框架設(shè)計(jì)人員來說,有兩種方法可以使其對(duì)用戶來說更優(yōu)雅,這樣他們就不必為閉包參數(shù)聲明顯式類型。第一個(gè)方法,也是最簡(jiǎn)單的方法,是用SAM類型替換閉包:
通過使用這種技術(shù),我們利用了Groovy將閉包自動(dòng)強(qiáng)制轉(zhuǎn)換為SAM類型的特性。
我們應(yīng)該使用SAM類型還是Closure的問題實(shí)際上取決于需要做什么。
在很多情況下,使用SAM接口就足夠了,特別是當(dāng)考慮Java 8中的功能接口時(shí)。
但是,閉包提供了功能接口無法訪問的特性。特別是,閉包可以有一個(gè)委托和所有者,并且可以在被調(diào)用之前作為對(duì)象進(jìn)行操作(例如,克隆、序列化、curry等等)。它們還可以支持多個(gè)簽名(多態(tài)性)。
因此,如果需要這種操作,最好切換到下面描述的最高級(jí)的類型推斷注釋。
當(dāng)涉及到閉包參數(shù)類型推斷時(shí),最初需要解決的問題是,Groovy類型系統(tǒng)繼承了Java類型系統(tǒng),而Java類型系統(tǒng)不足以描述參數(shù)的類型,也就是說,靜態(tài)地確定閉包的參數(shù)類型,而無需顯式地聲明它們。
2.3.3 使用@ClosureParams 注解
Groovy提供了一個(gè)注解@ClosureParams,用于完成類型信息。該注釋主要針對(duì)那些希望通過提供類型推斷元數(shù)據(jù)來擴(kuò)展類型檢查器功能的框架和API開發(fā)人員。如果我們的庫(kù)使用閉包,并且也希望獲得最大級(jí)別的工具支持,那么這一點(diǎn)非常重要。
讓我們通過修改原始示例來說明這一點(diǎn),引入@ClosureParams注釋:
@ClosureParams?注釋最少接受一個(gè)參數(shù),該參數(shù)被命名為類型提示。類型提示是一個(gè)類,它負(fù)責(zé)在閉包的編譯時(shí)完成類型信息。在本例中,使用的類型提示是groovy.transform.stc.FirstParam?,它向類型檢查器指示閉包將接受一個(gè)類型為方法第一個(gè)參數(shù)類型的參數(shù)。在本例中,方法的第一個(gè)參數(shù)是Person?,因此它向類型檢查器指示閉包的第一個(gè)參數(shù)實(shí)際上是Person。
第二個(gè)可選參數(shù)名為options。它的語義取決于類型提示類。Groovy提供了各種捆綁的類型提示,如下表所示:
類型提示 | 多態(tài) | 描述和示例 |
? | No | 第一個(gè)(回復(fù)。第二,第三)參數(shù)類型的方法: ? |
? | No | 第一個(gè)泛型類型(resp。第二,方法的第三)參數(shù) ? |
? | No | 閉包參數(shù)的類型來自選項(xiàng)字符串的類型提示。? |
? | Yes | 一個(gè)專用的閉包類型提示,可以在? |
? | Yes | 從某種類型的抽象方法推斷閉包參數(shù)類型。為每個(gè)抽象方法推斷一個(gè)簽名。? |
? | Yes | 從options參數(shù)推斷閉包參數(shù)類型。options參數(shù)由逗號(hào)分隔的非基元類型數(shù)組組成。數(shù)組中的每個(gè)元素都對(duì)應(yīng)一個(gè)簽名,元素中的每個(gè)逗號(hào)分別對(duì)應(yīng)簽名的參數(shù)。簡(jiǎn)而言之,這是最通用的類型提示,選項(xiàng)映射的每個(gè)字符串都像簽名文字一樣被解析。雖然這種類型提示非常強(qiáng)大,但如果可以的話必須避免,因?yàn)樗鼤?huì)由于解析類型簽名的必要性而增加編譯時(shí)間。??接受String的閉包的單個(gè)簽名::? |
即使你使用FirstParam?, SecondParam或ThirdParam作為類型提示,這并不嚴(yán)格意味著將傳遞給閉包的參數(shù)將是第一個(gè)(resp。方法調(diào)用的第二個(gè),第三個(gè))參數(shù)。這只意味著閉包的參數(shù)類型將與第一個(gè)(resp。方法調(diào)用的第二個(gè),第三個(gè))參數(shù)。
PS: 上面的表格,從Groovy中直接賦值的。所以表格閱讀比較難看
簡(jiǎn)而言之,在接受Closure?的方法上缺少@ClosureParams注釋不會(huì)導(dǎo)致編譯失敗。如果存在(它可以出現(xiàn)在Java源代碼中,也可以出現(xiàn)在Groovy源代碼中),則類型檢查器具有更多信息,并可以執(zhí)行額外的類型推斷。這使得框架開發(fā)人員對(duì)該特性特別感興趣。
第三個(gè)可選參數(shù)名為conflictResolutionStrategy。它可以引用一個(gè)類(從
ClosureSignatureConflictResolver擴(kuò)展而來),如果在初始推斷計(jì)算完成后發(fā)現(xiàn)了多個(gè)參數(shù)類型,則該類可以執(zhí)行額外的參數(shù)類型解析。Groovy提供了一個(gè)默認(rèn)類型解析器,它什么都不做,另一個(gè)則在找到多個(gè)簽名時(shí)選擇第一個(gè)簽名。解析器僅在發(fā)現(xiàn)多個(gè)簽名時(shí)調(diào)用,并且被設(shè)計(jì)為后處理器。任何需要注入類型信息的語句都必須傳遞一個(gè)通過類型提示確定的參數(shù)簽名。解析器然后從返回的候選簽名中選擇。
類型檢查器使用@DelegatesTo?注釋推斷委托的類型。它允許API設(shè)計(jì)者指示編譯器委托的類型和委托策略。@DelegatesTo注釋將在其他內(nèi)容中進(jìn)行專門的討論。這里就不擴(kuò)展了。
3. 靜態(tài)編譯
3.1 動(dòng)態(tài)與靜態(tài)
在類型檢查部分,我們已經(jīng)看到Groovy通過@TypeChecked?注釋提供了可選的類型檢查。類型檢查器在編譯時(shí)運(yùn)行,并對(duì)動(dòng)態(tài)代碼執(zhí)行靜態(tài)分析。無論是否啟用類型檢查,程序的行為都完全相同。這意味著@TypeChecked注釋對(duì)于程序的語義是中立的。盡管可能有必要在源中添加類型信息以使程序被認(rèn)為是類型安全的,但最終,程序的語義是相同的。
雖然這聽起來很好,但實(shí)際上有一個(gè)問題:在編譯時(shí)執(zhí)行的動(dòng)態(tài)代碼的類型檢查,根據(jù)定義,只有在沒有發(fā)生特定于運(yùn)行時(shí)的行為時(shí)才正確。例如,下面的程序通過了類型檢查:
有兩種計(jì)算方法。一個(gè)接受String?并返回int?,另一個(gè)接受int?并返回String?。如果你編譯這個(gè),它被認(rèn)為是類型安全的:內(nèi)部compute('foobar')?調(diào)用將返回一個(gè)int?,并且在這個(gè)int?上調(diào)用compute?將返回一個(gè)String。
現(xiàn)在,在調(diào)用test()之前,考慮添加以下行:
使用運(yùn)行時(shí)編程,我們實(shí)際上是在修改compute(String)?方法的行為,這樣它就不會(huì)返回所提供的參數(shù)的長(zhǎng)度,而是返回一個(gè)Date。如果執(zhí)行該程序,它將在運(yùn)行時(shí)失敗。因?yàn)檫@一行可以在任何線程的任何地方添加,所以類型檢查器絕對(duì)沒有辦法靜態(tài)地確保沒有這樣的事情發(fā)生。簡(jiǎn)而言之,類型檢查器很容易受到猴子修補(bǔ)的攻擊。這只是一個(gè)例子,但它說明了對(duì)動(dòng)態(tài)程序進(jìn)行靜態(tài)分析本質(zhì)上是錯(cuò)誤的。
Groovy為@typecheck?提供了另一種注釋,它實(shí)際上將確保被推斷為被調(diào)用的方法將在運(yùn)行時(shí)有效地被調(diào)用。該注釋將Groovy編譯器轉(zhuǎn)換為靜態(tài)編譯器,其中所有方法調(diào)用都在編譯時(shí)解析,生成的字節(jié)碼確保實(shí)現(xiàn)這一點(diǎn):注釋是@groovy.transform.CompileStatic。
3.2 @CompileStatic 注解
@CompileStatic?注釋可以添加到@TypeChecked?注釋可以使用的任何地方,也就是說,在類或方法上。沒有必要同時(shí)添加@TypeChecked和@CompileStatic?,因?yàn)锧CompileStatic?執(zhí)行@TypeChecked所做的一切,但是還會(huì)觸發(fā)靜態(tài)編譯。
讓我們以失敗的例子為例,但這一次讓我們用@CompileStatic?替換@TypeChecked注釋:
這是唯一的區(qū)別。如果我們執(zhí)行這個(gè)程序,這次就不會(huì)出現(xiàn)運(yùn)行時(shí)錯(cuò)誤。test?方法不再受猴子補(bǔ)丁的影響,因?yàn)樵谒闹黧w中調(diào)用的計(jì)算方法在編譯時(shí)是鏈接的,所以即使Computer的元類發(fā)生了變化,程序仍然按照類型檢查器的預(yù)期行事。
3.3 關(guān)鍵優(yōu)勢(shì)
在代碼中使用@CompileStatic有幾個(gè)好處:
- 類型安全
- 對(duì)猴子補(bǔ)丁(monkey patching)免疫
- 性能改進(jìn)
性能的提高取決于所執(zhí)行程序的類型。
如果它受I/O限制,靜態(tài)編譯代碼和動(dòng)態(tài)代碼之間的區(qū)別幾乎不明顯。
對(duì)于高度CPU密集型的代碼,由于生成的字節(jié)碼與Java為等效程序生成的字節(jié)碼非常接近(如果不是相等的話),因此性能得到了極大的提高。
4. 小結(jié)
到這里關(guān)于類型的相關(guān)知識(shí)就介紹完畢了,以上內(nèi)容可以通過Groovy官方文檔:Groovy Language Documentation (groovy-lang.org)了解更多知識(shí)。
PS:類型知識(shí)的介紹更多的是從各種概念定義等方面詳細(xì)介紹各種類型推斷的過程。我們其實(shí)可以簡(jiǎn)單了解。