sponsored links

一文讀懂,什麼是泛型程式設計

起源

泛型程式設計是一種程式設計風格,其中演算法以儘可能抽象的方式編寫,而不依賴於將在其上執行這些演算法的資料形式。這個概念在1989年首次由David Musser和Alexander A. Stepanov提出[1.參考]。

2011年,Alexander A. Stepanov和他的同事Deniel E. Rose出版的From Mathematics to Generic Programming一書中文版已出版對泛型程式設計進行更為精確的定義。

泛型程式設計是一種專注於對演算法及其資料結構進行設計的程式設計方式,它使得這些演算法即資料結構能夠在不損失效率的前提下,運用到最為通用的環境中。

一文讀懂,什麼是泛型程式設計

泛型程式設計的提出者

泛型這個詞並不是通用的,在不同的語言實現中,具有不同的命名。在Java/Kotlin/C#中稱為泛型(Generics),在ML/Scala/Haskell中稱為Parametric Polymorphism,而在C++中被叫做模板(Template),比如最負盛名的C++中的STL。任何程式設計方法的發展一定是有其目的,泛型也不例外。泛型的主要目的是加強型別安全和減少強制轉換的次數。

Java中的泛型程式設計

在Java中有泛型類和泛型方法之分,這些都是表現形式的改變,實質還是將演算法儘可能地抽象化,不依賴具體的型別。

generics add a way to specify concrete types to general purposes classes and methods that operated on Object before

通用的類和方法,具有代表性的就是集合類。在Java1.5之前,Java中的泛型都是透過單根繼承的方式實現的。比如:

public class ArrayList // before Java SE 5.0
{
public Object get(int i)
public void add(Object o)
public boolean contains(Object o);
private Object[] elementData;
}

雖然演算法足夠通用了,但是這樣會帶來兩個問題。一個是型別不安全,還有一個是每次使用時都得強制轉化。減少型別轉換次數比較容易理解,在沒有泛型(引數化型別)的時候,裝進容器的資料,其型別資訊丟失了,所以取出來的時候需要進行型別轉換。 例如:

List list = new ArrayList();
list.add(1);

assertThat(list.get(0), instanceOf(Integer.TYPE));
assertThat((Integer)list.get(0), is(1)); //存在強制轉換

因為這個類裡只有Object的宣告,所以任意型別的物件都可以加入到這個集合當中,在使用過程中就會存在強制到具體的型別失敗的問題,這將喪失編譯器檢查的好處。

List list = new ArrayList();
list.add(1);
list.add("any type");

assertThat(list.get(1), instanceOf(String.class));
assertThat((Integer) list.get(1), is(1));//-> java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer

2005 Java SE 5引入了泛型,不僅有效地提高了演算法的通用程度,同時也保留強型別語言在編譯期檢查的好處。

Generics This long-awaited enhancement to the type system allows a type or method to operate on objects of various types while providing compile-time type safety. It adds compile-time type safety to the Collections Framework and eliminates the drudgery of casting.

所以上述的程式會寫成這樣:

List<Integer> list = new ArrayList<Integer>();
list.add(1);
// list.add("no way"); 編譯出錯
assertThat(list.get(0), instanceOf(Integer.TYPE));
assertThat(list.get(0), is(1)); // 不需要強制轉換

型別安全

在靜態強型別語言中,編譯期間的檢查非常重要,因為它可以有效地避免低階錯誤。這些低階錯誤就是型別安全解決的問題。型別安全包含了賦值安全和呼叫安全。其底層實質上就是在某塊記憶體中,始終存在被同種型別的指標指向。

  1. 型別賦值檢查
long l_num = 1L;
int i_num = l_num; // 編譯錯誤

在強型別的語言當中,型別不一致是無法互相賦值的。

\2. 型別呼叫檢查 Clojure就是一門強型別語言,而且還是一門函式式語言,所以重新賦值不被允許,它的型別安全表現在針對型別的呼叫安全。

user=> (+ "" 1)
...
java.lang.ClassCastException: java.lang.String cannot be castto java.lang.Number

這裡存在一個隱式型別轉化的過程,但是由於String無法轉化成Number,所以方法呼叫失敗。由於Clojure是動態語言,所以只有在執行時才會丟擲錯誤。

另一個簡單的例子,如果一個型別不存在某個方法,那就沒法去呼叫它。在動態強型別語言中,執行時一定會報錯。其實質是型別是記憶體堆上的一塊區域,如果該區域之上沒有想要呼叫的方法,那麼呼叫在編譯期或者執行期間一定會出錯。

new Object().sayNothing() // 編譯出錯

為什麼說型別安全對於開發人員友好,這個特性對於程式語言很重要?其實這可以追溯到三次程式設計正規化解決的根本問題上。Clean Architecture(架構整潔之道)一書中,對結構化,面向物件和函數語言程式設計語言做了很透徹的分析。

首先我們得明確一點,這些正規化從來沒有擴充套件程式語言的能力,而是在不同方面對程式語言的能力進行了約束。

  1. 結構化程式設計 對程式的直接控制進行約束和規範,goto considered harmful.
  2. 面向物件程式設計 對程式的間接控制進行約束和規範,pointer considered harmful.
  3. 函數語言程式設計 對程式的賦值進行約束和規範,mutability considered harmful.

按照這樣的思路,泛型程式設計無非是對既有的正規化做了進一步的約束。泛型程式設計旨在對程式的間接控制進一步進行約束和規範。它把型別安全放在第一位,而將型別轉化限制在編譯期間。

我們甚至可以遵循前面的定義方式,說: 2.1 泛型程式設計 對程式的間接控制進一步進行約束和規範,type casting considered harmful.

Kotlin中的泛型程式設計

一文讀懂,什麼是泛型程式設計

variance - 變化

和Java泛型中的泛型方法和泛型類概念類似,Kotlin將對應的概念稱為引數化函式和引數化型別。

parameterized function 引數化函式

假設我們要返回三個物件中任一一個物件,同時保證型別一致。引數化函式是很恰當的選擇。

fun <T> random(one: T, two: T, three: T): T

parameterized type 引數化型別

除了引數化函式,型別本身也可以定義自己的引數化型別。比如:

class Dictionary<K, V>

bounded polymorphism 限定引數化型別

大部分情況下,引數化型別不會是無限抽象的,無限抽象往往不利於語言的表達性。所以限定的引數化型別應運而生。

fun <T : Comparable<T>> min(first: T, second: T): T {
val k = first.compareTo(second)
return if (k <= 0) first else second
}

如果需要用多個邊界來限定型別,則需要用到where語句,表達T被多個邊界類或者介面限制。

class MultipleBoundedClass<T> where T : Comparable<T>, T : Serializable

invariance 不變

一文讀懂,什麼是泛型程式設計

open class Animal
class Dog : Animal()
class Cat : Animal()
class Box<T>(val elements: MutableList<T>) {
fun add(t: T) = elements.add(t)
fun last(): T = elements.last()
}

fun foo(box: Box<Animal>){
}

val box = Box(mutableListOf(Dog()))
// -> val box: Box<Dog> = Box(mutableListOf(Dog()))
box.add(Dog()) // ok
box.add(Cat()) // 編譯錯誤

這裡出現的編譯錯誤,原因是box的真實型別是Box<Dog>,所以嘗試向中新增Cat物件是不會成功的。這樣總能保證型別安全。

Dog是Animal的子型別,那麼編譯器是否承認是Box<Animal>的子型別,在使用時進行隱式轉換呢?

val box: Box<Animal> = Box(mutableListOf(Dog()))
// type inference failed. Expected type mismatch.

編譯器是不會允許這樣行為發生。原因就是這樣做會導致型別不安全。我們試想一下,假如這種轉換是允許的,也就是說,我們承認了是的子類,那麼我們就可以繼續新增其它繼承了的子類物件,比如:

val box: Box<Animal> = Box(mutableListOf(Dog())
box.add(Cat()) // 由於對外的介面是 Animal,所以意外接收的是 Cat,但是例項方法要求的是 Dog。

這樣就導致子類的例項方法add(t: Dog)(注意,這裡的 T 被具現化成了 Dog)方法接收了一個型別的引數,這顯然會丟擲ClassCastException,所以這是非型別安全的。編譯器不會容許這種事情發生,所以報出編譯錯誤,如下:

val box: Box<Dog> = Box(mutableListOf(Dog()))
val animalBox: Box<Animal> = box // 編譯錯誤

還有一種情況,dog是的子型別,那麼可不可以是的子型別呢?即:

val box: Box<Dog> = Box(mutableListOf(Animal())
val dog: Dog = box.last() //由於對外的介面是 Dog,所以返回的是 Dog,但是例項方法要求的是 Animal。

如此會導致子類的例項方法last(): Animal方法返回了一個型別的返回值,但是不是所有的 Animal 都是 Dog,這也會導致,所以編譯器會阻止這種使用方式,報出編譯錯誤,如下:

val box: Box<Animal> = Box(mutableListOf(Animal()))
val dogBox: Box<Dog> = box // 編譯錯誤

不過,總的來說,不變的限制太過於嚴苛了,泛型的設計既要考慮型別安全性,也要對靈活性有所關照。

對於前一種情況,我們只需要從這個box“讀取”元素,即泛型引數作為方法的返回值,而不需要往裡面“新增”元素,即泛型引數不能作為方法的引數,那麼這種轉換就是型別安全的。而後一種情況,我們規定只能往box裡“新增”元素,但不“讀取”元素,那麼也能到達同樣的目的。故而,協逆變出現了。

covariance 協變

一文讀懂,什麼是泛型程式設計

如上圖所示,當是的子型別,那麼也是的子型別,這種繼承關係就是協變。在Kotlin中,我們需要使用out關鍵字表示這種關係。

class CovarianceBox<out T : Animal>(val elements: MutableList<out T>) {
fun add(t: T) = elements.add(t) //編譯錯誤
fun last(): T = elements.last()
}

基於這種協變關係,我們可以這樣呼叫

val dogs: CovarianceBox<Dog> =CovarianceBox(mutableListOf(Dog(), Dog()))
val animals: CovarianceBox<Animal> = dogs
print(animals.last())

我們注意上面的CovarianceBox的add方法出現了編譯錯誤,原因就是在協變關係中,泛型引數只能作為輸出引數,而不能作為輸入引數。因為在拒絕了輸入泛型引數的前提下,協變發生的時候,才不會出現強制轉化的錯誤,這裡的原因和不變的第一種情況是一樣的。

不過,這種解決方式也不是萬能的,屬於殺敵一千,自損八百的戰術。因為對於Collection而言,不可能做到任何泛型引數都不會出現在入參的位置上。

public interface Collection<out E> : Iterable<E> {
public operator fun contains(element: @UnsafeVariance E): Boolean
public fun containsAll(elements: Collection<@UnsafeVarianceE>): Boolean
}

所以,針對這種情況,我們知道某些方法其實並不會有新增的操作,可以在入參的位置上加上@UnsafeVariance,以此消除掉編譯器的錯誤。

contravariance 逆變

一文讀懂,什麼是泛型程式設計

contravariance 逆變

當Dog是Animal的子型別,那麼Box<Animal>也是Box<Dog>的子型別,這種繼承關係就是逆變。在Kotlin中,我們需要使用in關鍵字表示這種關係。

class ContravarianceBox<in T>(val elements: MutableList<inT>) {
fun add(t: T) = elements.add(t)
fun first(): T = elements.first() // 編譯錯誤
}

基於這種逆變關係,我們可以這樣呼叫

val animals = ContravarianceBox(mutableListOf(Animal()))
val dogs: ContravarianceBox<Dog> = animals
dogs.add(Dog()) // 編譯透過

這個時候,型別始終是安全的。但是我們也注意到ContravarianceBox的first方法出現了編譯錯誤,原因就是在逆變關係中,泛型引數只能作為輸入引數,而不能作為輸出引數。在拒絕了輸出引數的前提下,逆變發生的時候,才不會出現強制轉換的錯誤,具體理由同上述的不變原因一致。

val animals = ContravarianceBox(mutableListOf(Animal()))
val dogs: ContravarianceBox<Dog> = animals
dogs.add(Dog())
val dog: Dog = dogs.first() // 編譯錯誤

reification 變現

reify is To convert mentally into a thing; to materialize.

Kotlin中的Reification的實現使用的是inline模式,就是在編譯期間將型別進行原地替換。

// 定義
inline fun <reified T : Any> loggerFor(): Logger =LoggerFactory.getLogger(T::class.java)
// 使用
private val logger = loggerFor<AgreementFactory>()

因此,所以原來呼叫處的程式碼會在編譯期間展開成如下:

private val logger = LoggerFactory.getLogger(AgreementFactory::class.java)

使用reification操作,可以精簡掉很多模板程式碼。

type projection 型別投影

一文讀懂,什麼是泛型程式設計

type projection 型別投影

上述過程中,我們看到協變和逆變都是針對可以編輯的類。但是如果遇到已經存在的類,這件事就得運用型別投影技術。拿Class這個類舉例:

val dog = Dog::class.java
val animal: Class<Animal> = dog //編譯不透過

Kotlin中的type projection就是為了解決這個問題的。

val dog = Dog::class.java
val animal: Class<out Animal> = dog

同理,

val animal = Animal::class.java
val dog: Class<in Dog> = animal

我們來看一個真實的場景,

val agreementClass: Class<RentalAgreement> =RentalAgreement::class.java

private val virtualTable = mapOf(RentalPayload.type toRentalAgreement::class.java)
private fun dispatch(type: String): Class<outAgreement<Payload>> {
return virtualTable[type]
?: throw RuntimeException("No suitable Agreement of this type found, please check your type: $type")
}

只有這樣,我們才能將具體的Class<RentalAgreement>投射到Class<out Agreement<Payload>>父型別之上,後續透過某種方式,例項化出RentalAgreement的例項,其繼承自Agreement<Payload>。

泛型程式設計的思考

一文讀懂,什麼是泛型程式設計

過程式程式碼 vs. 面向物件

Bob 大叔的 Clean Code 一書的第六章《物件和資料結構》中提到了一個很有意思的現象:資料、物件的反對稱性。在這裡,資料結構暴露資料,沒有提供有意義的函式;物件把資料隱藏起來,暴露操作資料的函式。

過程式程式碼會基於資料結構進行操作。例如:首先會定義好資料結構Square, Circle和Triangle,然後統一在area(shape: Any)的函式中求shape資料的面積,如:

fun area(shape: Any): Double {
return when(shape) {
is Square -> return shape.side * shape.side
else -> 0.0
}
}

而面向物件擁躉一定會嗤之以鼻——顯然應該抽象出一個shape類包含area方法,讓其它的形狀類繼承。如:

interface Shape {
fun area(): Double
}

class Square(val side: Double) : Shape {
override fun area(): Double {
return side * side
}
}

在新增新的形狀的要求下,面向物件的程式碼是優於過程式的,因為面向物件對型別的擴充套件開放了。而過程式程式碼卻不得不修改原來area方法的實現。

但是,如果此時需要新增一個求周長primeter的函式。相對於面向物件程式碼,過程式程式碼由於無需修改原來的實現,反而更加容易擴充套件。反觀面向物件的程式碼,在介面Shape中新增一個primeter會導致所有的子類都得發生修改。

這就是資料和型別的反對稱性。在變化方向不同的時候,它們面臨的阻力也是不一樣的。

一文讀懂,什麼是泛型程式設計

隔離阻抗

我們既想要過程式對方法擴充套件的優點,又執著面向物件自然的型別擴充套件的好處,該怎麼辦呢?可以考慮結合起來使用。

這樣的結合不是說原有的雙向阻力消失了,而是在不同的層次上應用各自的優點。也就是說,Shape需要求面積、周長,同時也要支援型別擴充套件,這種要求之下,基本不可能調解出一種符合開閉原則的方案。不過,如果對於所有Shape類,都需要統一進行某些操作,例如:集合的排序,過濾等等。那麼合併兩者的好處就變得顯著起來。

一文讀懂,什麼是泛型程式設計

泛型補充

基於最先分析的透過繼承的方式進行泛型程式設計的缺點:

  1. 太多強制轉換
  2. 非型別安全。 恰當地引入了泛型T,以期編譯期的佔位和執行時的替換。

一文讀懂,什麼是泛型程式設計

泛型限定

不過沒有限定的泛型大部分情況下是沒有用處的,因為無限的抽象沒有意義,所以需要更加精準的泛型限定。

一文讀懂,什麼是泛型程式設計

依賴倒置

在我們做完這一切以後,會驚喜地發現依賴倒置(DIP)原則貫穿始終。不論是繼承體系,還是改善之後的泛型繼承體系。它們秉持的原則就是在編譯期,始終朝著穩定、抽象的方向移動,而且不斷在易變、具體的方向延遲決策,直到執行時方能確定。

書籍推薦

一文讀懂,什麼是泛型程式設計

書籍推薦

腦圖

一文讀懂,什麼是泛型程式設計

知識梳理

參考連結

泛型 一個會寫詩的程式設計師

可執行程式碼示例

分類: 新聞
時間: 2021-09-20

相關文章

一文讀懂資產負債表(上)

一文讀懂資產負債表(上)
資產負債表反映的是一家公司在某個時點上可以以貨幣計量的資產.負債及所有者權益情況.資產負債表是財務狀況在某一特定時點上的快照,它反映的只是那一瞬間的資訊.它分為3個部分:資產,負債和所有者權益. 資產 ...

一文讀懂健康骨骼核心資訊

一文讀懂健康骨骼核心資訊
一文讀懂 健康骨骼核心資訊 固定佈局 工具條上設定固定寬高 背景可以設定被包含 可以完美對齊背景圖和文字 以及製作自己的模板 認識骨質疏松症# 骨質疏鬆症是中老年人最常見的一種全身性骨骼疾病,疼痛.駝 ...

【商品檢驗】一文讀懂安全座椅那些事兒

【商品檢驗】一文讀懂安全座椅那些事兒
一文讀懂安全座椅那些事兒 暑假來臨,家長們一定提前給小朋友們準備了豐富多彩的戶外節目,如叢林探險.海邊衝浪.星空露營等等.但是,駕車出遊前,千萬要記得讓小朋友坐安全座椅! 新修訂的<未成年人保護 ...

野釣成為高手的標誌:會做窩,一文讀懂做窩的思路

野釣成為高手的標誌:會做窩,一文讀懂做窩的思路
這些年蓑笠哥寫過一些關於打窩的內容,但是一直分享如何做窩的內容.主要是因為自己感覺還無法駕馭.所經歷的釣況還不夠多.經驗還有待完善. 當然,前面這些哪怕是到了現在也不敢說精通二字,可能只是掌握了一點皮 ...

一文讀懂子不語IPO:跨境電商「黑馬」年利潤過億

一文讀懂子不語IPO:跨境電商「黑馬」年利潤過億
"截至2020年底,子不語自主設計品牌數量已達151個,自營網站收入佔比大增." 本文為IPO早知道原創 作者|蘇打 疫情的持續蔓延,在衝擊許多行業發展軌跡的同時,也顛覆著線上的消 ...

開啟在即!一文讀懂蘇寧易購家電家裝購物節

開啟在即!一文讀懂蘇寧易購家電家裝購物節
9月16日,蘇寧易購南部片區在深圳華強北群星廣場舉辦了區域變革以來的首場釋出會.會上,蘇寧易購集團南區管理總部執行總裁戴馮軍進一步明確,蘇寧易購將圍繞三大攻略.四大舉措賦能南區本地,惠及南區千萬家. ...

一文讀懂手機應用簡史

一文讀懂手機應用簡史
在科技史上.一個東西如果涉及了生活的大部分方面.甚至可以誇張點說,所有方面.基本上這個應用也就到頭了.以後的變化就是不斷的迭代升級.從這一點看,手機的應用是已經到頭了.手機可以控制冰箱.電視.電燈.買 ...

汽車BOM指什麼?一文讀懂汽車BOM管理

汽車BOM指什麼?一文讀懂汽車BOM管理
整車BOM是汽車生產企業的主資料,貫穿從設計到銷售的各個方面.根據市場定位及產品特點,汽車生產企業採用符合自身特點的BOM管理方式.今天帶大家分析了目前一些汽車生產企業的主要BOM管理方式,明確各管理 ...

購買iphone13指南!一文讀懂超詳細

購買iphone13指南!一文讀懂超詳細
今年的幾款蘋果系列處理器都還有區別. iphone13p和13pm,gpu圖形處理器都是5核 而13gpu圖形處理器是4核 而且今年的13真不香,執行記憶體iphone13是4g運存,而13p和13p ...

木材人看這裡,一文讀懂中國木材主要來源國現狀

木材人看這裡,一文讀懂中國木材主要來源國現狀
近年來隨著我國生態環境保護力度的不斷加強,森林面積在不斷增加,但在森林消耗方面卻嚴格控制,我國木材產量雖然有所增加,卻無法滿足國內需求,中國目前是全球最大的木製品進口商.製造商和出口商之一,特別是中國 ...

「招飛資訊」一文讀懂民航招飛與空軍招飛!記得收藏

「招飛資訊」一文讀懂民航招飛與空軍招飛!記得收藏
來源:網路. 本文內容由舒伯生涯[微信公眾號:careerschool]整理,轉載請註明出處,舒伯生涯尊重版權,如有侵權問題,請及時聯絡管理員刪除. 空軍招飛.民航招飛的相關資訊一般於每年9月開始陸續 ...

尿常規能查出什麼病?一文讀懂體檢單!

尿常規能查出什麼病?一文讀懂體檢單!
"醫生,每次體檢尿常規都正常,這次能不驗嗎?" "醫生,我來大姨媽了還能驗尿常規嗎?" "醫生,這個尿常規怎麼看呀?" 尿常規是我們常說的& ...

一文讀懂糖玉,從糖玉的“糖”看和田玉

一文讀懂糖玉,從糖玉的“糖”看和田玉
糖玉是和田玉中較為特色的一種,特別適合特色巧雕,造型顏色特殊的糖玉也更能激發玉雕師的想象力.和田玉糖玉中糖白玉料是非常難得的,玉質上乘的糖白玉,它的白的部位一定是瑩潤剔透的,而糖色浸入玉白的部分一定是 ...

一文讀懂:如何享受小型微利企業減免企業所得稅優惠政策
近年來,黨中央.國務院高度重視小微企業.個體工商戶發展,出臺了一系列稅費支援政策,持續加大減稅降費力度.為便利小微企業和個體工商戶及時瞭解適用稅費優惠政策,稅務總局對針對小微企業和個體工商戶的稅費優惠 ...

在河南發現3.2萬年前的人類頭骨化石究竟意味著什麼?一文讀懂

在河南發現3.2萬年前的人類頭骨化石究竟意味著什麼?一文讀懂
大家都知道,我國的中原地區是華夏文明和中華文明的發源地,擁有深厚的人類文化積澱. 不久前,國家文物局宣佈,在河南省平頂山市魯山縣的仙人洞遺址,發現了距今3.2萬年的人類頭骨化石.這個發現意味著3萬多年 ...

一文讀懂:糖尿病 12 大謠言,大家別再相信了

一文讀懂:糖尿病 12 大謠言,大家別再相信了
我們都知道謠言止於智者,但現代社會資訊發達,許多"驚人謠言"在各大社交平臺上被瘋狂轉發,讓人分不清真假. 今天小編就給大家羅列一些經常聽到的謠言,希望以後大家再被迷惑哦! 謠言1: ...

一文讀懂窗臺石!蘭潤裝飾

一文讀懂窗臺石!蘭潤裝飾
窗臺石就是窗臺上的材料,包括飄窗和窗臺. 窗臺石對陽臺有著保護作用,能夠避免雨水濺落到窗臺面上引起脫落和老化的現象,避免雨水汙染內部牆面.窗臺石的材質.色彩豐富多元,也能起到一定的裝飾作用. 常見的窗 ...

尿布該背鍋嗎?一文讀懂尿布皮炎
沒生娃前, 好姐妹聊天都是"買!買!買!" 生娃之後, 好姐妹聊天就變成"煩!煩!煩!" 最讓新手寶媽揪心的, 莫過於寶寶的紅屁股. 看著小屁股又紅又腫, 當媽 ...

讀懂張愛玲,就讀懂了中國式親戚

讀懂張愛玲,就讀懂了中國式親戚
有人說:中國親戚有個奇異功能,不管在聊什麼話題,聊著聊著,話題就變成了,大齡的怎麼還不結婚,結婚的怎麼還不生娃? 以致於逢年過節,很多人都有了恐親戚症. 中國社會,是以親戚關係組成的關係網. 說起七大 ...