限定通配符和非限定通配符_为什么我不信任通配符以及为什么我们仍然需要通配符...
限定通配符和非限定通配符
在將子類型多態(tài)性(面向?qū)ο?#xff09;與參數(shù)多態(tài)性(泛型)相結(jié)合的任何編程語言中,都會出現(xiàn)方差問題。 假設(shè)我有一個字符串列表,鍵入List<String> 。 我可以將其傳遞給接受List<Object>的函數(shù)嗎? 讓我們從這個定義開始:
破碎的協(xié)方差
憑直覺,我們可能首先認(rèn)為應(yīng)該允許這樣做。 看起來不錯:
void iterate(List<Object> list) {Iterator<Object> it = list.iterator();... } iterate(ArrayList<String>());確實,包括Eiffel和Dart在內(nèi)的某些語言確實接受此代碼。 可悲的是,它是不完善的,如以下示例所示:
//Eiffel/Dart-like language with //broken covariance: void put(List<Object> list) {list.add(10); } put(ArrayList<String>());在這里,我們將List<String>傳遞給接受List<Object>的函數(shù),該函數(shù)嘗試將Integer添加到列表中。
Java對數(shù)組也會犯同樣的錯誤。 以下代碼編譯:
//Java: void put(Object[] list) {list[0]=10; } put(new String[1]);它在運行時失敗,并帶有ArrayStoreException 。
使用地點差異
但是,對于通用類和接口類型,Java采用了不同的方法。 默認(rèn)情況下,類或接口類型為invariant ,即:
- 當(dāng)且僅當(dāng)U與V類型完全相同時,才可將L<V>分配給L<V> 。
由于這在很多時候非常不方便,因此Java支持一種稱為use-sitevariance的方法 ,其中:
- L<U>可分配給L<? extends V> 如果U是V的子類型,則L<? extends V> ,并且
- L<U>可分配給L<? super V> L<? super V>如果U是的超類型V 。
丑陋的語法? extends V ? extends V或? super V ? super V稱為通配符 。 我們還說:
- L<? extends V> L<? extends V>在V是協(xié)變的,并且
- L<? super V> L<? super V>在V是反變的。
由于Java的通配符表示法非常丑陋,因此在本討論中我們將不再使用它。 取而代之的是,我們將分別使用關(guān)鍵字in和out來表示通變量和協(xié)方差。 從而:
- L<out V>在V是協(xié)變的,并且
- L<in V>是在逆變 V 。
給定的V稱為通配符的邊界 :
- out V是一個上限通配符, V是其上限,并且
- in V是下界通配符, V是其下界。
從理論上講,我們可以有一個具有上限和下限的通配符,例如L<out X in Y> 。
我們可以使用交集類型表示多個上限或多個下限,例如L<out U&V>或L<in U&V> 。
請注意,類型表達(dá)式L<out Anything>和L<in Nothing>指的是完全相同的類型,并且此類型是L的所有實例的超類型。 您會經(jīng)常看到人們將通配符類型稱為存在性類型 。 他們的意思是,如果我知道list的類型為List<out Object> :
然后我知道存在一個未知的類型T ,這是Object的子類型,因此list的類型為List<T> 。
或者,我們可以從更寬泛的角度出發(fā),說List<out Object>是所有List<T>類型的并集,其中T是Object的子類型。
在具有使用地點差異的系統(tǒng)中,以下代碼無法編譯:
但是這段代碼可以做到:
void iterate(List<out Object> list) {Iterator<out Object> it = list.iterator();... } iterate(ArrayList<String>());正確地,此代碼無法編譯:
void put(List<out Object> list) {list.add(10); //error: Integer is not a Nothing } put(ArrayList<String>());現(xiàn)在我們在兔子洞的入口。 為了將通配符類型集成到類型系統(tǒng)中,同時像上面的示例一樣拒絕不正確的代碼,我們需要一種更為復(fù)雜的算法來替換類型實參。
會員輸入使用地點差異
也就是說,當(dāng)我們有一個泛型類型類似List<T>有一種方法void add(T element) ,而不是僅僅直截了當(dāng)代Object的T ,就像我們做普通不變的類型,我們需要考慮的方差類型參數(shù)出現(xiàn)的位置。 在這種情況下, T出現(xiàn)在List類型的反位置 ,即作為方法參數(shù)的類型。 我不會在這里寫下的復(fù)雜算法告訴我們,在此位置我們應(yīng)該用Nothing (底部類型)代替。
現(xiàn)在想象一下我們的List接口有一個帶有以下簽名的partition()方法:
List<out Y>的partition()的返回類型是什么? 好吧,在不損失精度的情況下,它是:
List<in List<in Y out Nothing> out List<in Nothing out Y>> 哎喲。
由于沒有人在他們的頭腦中想去考慮這樣的類型,因此明智的語言會拋棄其中的一些界限,而留下這樣的東西:
這是可以接受的。 不幸的是,即使在這種非常簡單的情況下,我們也已經(jīng)遠(yuǎn)遠(yuǎn)超出了程序員可以輕松跟隨類型檢查器所做的工作的地步。
因此,這就是我不信任使用地點差異的原因所在:
- Ceylon設(shè)計的一個重要原則是,程序員應(yīng)始終能夠重現(xiàn)編譯器的推理。 這是原因的一些與使用現(xiàn)場方差出現(xiàn)的復(fù)雜類型的非常困難。
- 它具有病毒性作用:一旦這些通配符類型在代碼中立足,它們便開始傳播,很難回到我的普通不變式類型。
申報地點差異
使用場所方差的一種更合理的選擇是聲明場所方差 ,在聲明時我們指定泛型類型的方差。 這是我們在錫蘭使用的系統(tǒng)。 在此系統(tǒng)下,我們需要將List分為三個接口:
interface List<out T> {Iterator<T> iterator();List<List<T>> partition(Integer length);... }interface ListMutator<in T> {void add(T element); }interface MutableList<T>satisfies List<T>&ListMutator<T> {} List聲明為協(xié)變類型, ListMutator聲明為協(xié)變類型, ListMutator聲明為MutableList的不變子類型。
似乎對多個接口的需求似乎是聲明站點差異的一個很大的缺點,但事實證明,將突變與讀取操作分開是很有用的,并且:
- 變異運算通常是不變的,而
- 讀取操作通常是協(xié)變的。
現(xiàn)在我們可以這樣編寫函數(shù):
void iterate(List<Object> list) {Iterator<Object> it = list.iterator();... } iterate(ArrayList<String>());void put(ListMutator<Integer> list) {list.add(10); } put(ArrayList<String>()); //error: List<String> is not a ListMutator<Integer>您可以在此處閱讀有關(guān)聲明位置差異的更多信息。
為什么我們在錫蘭需要使用場所差異
可悲的是,Java沒有聲明站點差異,并且與Java的良好互操作對我們來說非常重要。 我不喜歡純粹為了與Java互操作而在語言的類型系統(tǒng)中添加主要功能,因此多年來,我一直拒絕向Ceylon添加通配符。 最后,現(xiàn)實和實用性獲勝,而我的頑固失去了。 因此,Ceylon 1.1現(xiàn)在具有帶有單界通配符的使用站點差異。
我試圖盡可能嚴(yán)格地限制此功能,僅提供體面的Java互操作所需的最低限度的功能。 這意味著,就像在Java中一樣:
- 沒有形式為List<in X out Y>雙界通配符,并且
- 在類或接口定義的extends或satisfies子句中不能出現(xiàn)通配符類型。
此外,與Java不同:
- 沒有隱式界通配符,上限必須始終以顯式形式編寫,并且
- 不支持通配符捕獲 。
通配符捕獲是Java的一個非常聰明的功能,它利用了通配符類型的“現(xiàn)有”解釋。 給定這樣的通用函數(shù):
List<T> unmodifiableList<T>(List<T> list) => ... :Java讓我調(diào)用unmodifiableList() ,傳遞一個通配符類型,如List<out Object> ,返回另一個通配符List<out Object> ,原因是存在一些未知的X ,這是Object的子類型,對其進(jìn)行調(diào)用是正確的。 也就是說,即使無法為任何T將List<out Object>類型分配給List<T> ,也認(rèn)為該代碼是正確的:
List<out Object> objects = .... ; List<out Object> unmodifiable = unmodifiableList(objects);在Java中,涉及通配符捕獲的鍵入錯誤幾乎是無法理解的,因為它們涉及未知且難以理解的類型。 我沒有計劃向錫蘭添加對通配符捕獲的支持。
試試看
使用站點差異已經(jīng)實現(xiàn),并且已經(jīng)在Ceylon 1.1中起作用,如果您非常有動力,可以從GitHub獲得。
即使此功能的主要動機(jī)是出色的Java互操作性,但在其他情況(可能很少見)中,通配符將很有用。 但是,這并不表示我們的方法有任何重大變化。 除極端情況外,我們將繼續(xù)在Ceylon SDK中使用聲明站點差異。
更新: 我只是意識到我忘了感謝Ross Tate的幫助,幫助我更好地了解了成員打字算法中使用地點差異的問題。 羅斯知道這些非常棘手的東西!
翻譯自: https://www.javacodegeeks.com/2014/08/why-i-distrust-wildcards-and-why-we-need-them-anyway.html
限定通配符和非限定通配符
總結(jié)
以上是生活随笔為你收集整理的限定通配符和非限定通配符_为什么我不信任通配符以及为什么我们仍然需要通配符...的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 百草枯哪里买
- 下一篇: JMetro版本11.5.11和8.5.