一、引言
類型擦除是一個比較難以理解的概念(如果只是粗淺的理解,那麼也不算太難)。如果要深入理解,涉及到:OOP、POP、逆變、協變、泛型編程、編譯器等概念。雖然有不少文章都在講類型擦除,但是一般存在有以下的一些問題:
- 示例代碼無法編譯
- 講解過程過於理論化(程序員大多是從實現、技術角度去理解一個概念的,而不是從理論角度理解)
- 直接給出最終結論,使讀者不易接受
- 講述過程不清不楚
本文作者有嘗試深入分析一下類型擦除,希望讀者看了之後,能夠達到觀止的效果。
二、面向對象的劣勢
假如,我們要編寫一系列生產各種汽車的工廠,那麼代碼如下。
// 定義車的類型
typealias CarType = String
// 定義錯誤的類型
let ErrorCarType : CarType = ""
// 車的基類
class BaseCar {
func CarType() -> CarType {
return ErrorCarType
}
}
// 特斯拉
class Tesla : BaseCar {
override func CarType() -> CarType {
return "Tesla"
}
}
// 長城
class GreatWall : BaseCar {
override func CarType() -> CarType {
return "HaFo"
}
}
// 汽車工廠的基類
class BaseCarFactory {
func createCar() -> BaseCar? {
return nil
}
}
// 特斯拉汽車工廠的基類
class TeslaCarFactory : BaseCarFactory{
override func createCar() -> BaseCar? {
return Tesla()
}
}
// 長城汽車工廠的基類
class GreatWallCarFactory : BaseCarFactory{
override func createCar() -> BaseCar? {
return GreatWall()
}
}
從以上代碼來看,汽車使用到了繼承,汽車工廠也使用到了繼承。但是我們知道,繼承作爲面向對象編程的一大特型,其是一種強依賴,是一種不好的做法。在面向對象編程中,應儘量避免繼承;但是,實際情況是,繼承還是不可避免的被濫用。
注意
上述代碼是一個示例,核心目的是展示——繼承在面向對象編程中被濫用的情況。
三、面向協議編程的解決之道
在面向協議編程中,協議的作用大大加強,很多情況下,通過協議來代替父類的存在,從而規避繼承的弊端。
在上述代碼中引入協議。
typealias CarType = String
let ErrorCarType : CarType = ""
protocol Car {
func CarType() -> CarType
}
class Tesla : Car {
func CarType() -> CarType {
return "Tesla"
}
}
class GreatWall : Car {
func CarType() -> CarType {
return "HaFo"
}
}
protocol CarFactory {
func createCar() -> Car?
}
class TeslaCarFactory : CarFactory {
func createCar() -> Car? {
return Tesla()
}
}
class GreatWallCarFactory : CarFactory {
func createCar() -> Car? {
return GreatWall()
}
}
注意
上述代碼通過把汽車、汽車工廠的基類替換爲協議的方式解決了在面向對象編程時的繼承濫用問題。
四、泛型編程登場
經過面向協議編程的改造,上述代碼去除了繼承的濫用,代碼質量有了大幅度的提高。但是,依然有進步的空間。
上述汽車工廠類返回的是一個協議對象,該對象屏蔽了具體汽車工廠生成的汽車類型。某些時候這正是你想要的,某些時候這不是向你要的,但是你不知道這不是你想要的,只是沿着面向對象編程的肌肉記憶,永遠不停的前進。
class Tesla {
let brand = "Tesla"
}
class GreatWall {
let brand = "GreatWall"
}
protocol CarFactory {
// 關聯類型
associatedtype CarType
func createCar() -> CarType
}
class TeslaCarFactory : CarFactory {
// 關聯類型是Tesla
func createCar() -> Tesla {
return Tesla()
}
}
class GreatWallCarFactory : CarFactory {
// 關聯類型是GreatWall
func createCar() -> GreatWall {
return GreatWall()
}
}
經過上述改造之後,通過爲CarFactory添加關聯類型,實現了具體汽車工廠返回具體類型的功能。某些情況下這正是你想要的。
五、具有關聯類型的協議
上述代碼中,CarFactory包含了關聯類型。也有許多講關聯類型的文章,很多很有用,但是也很多把簡單的事情,給弄複雜了,使作者不甚理解,甚至越看越糊塗。
簡單來說:Swift不支持常見形式的泛型協議,通過關聯類型來實現協議的泛型
// Protocols do not allow generic parameters; use associated types instead
// 這就是常見形式的泛型,但是Swift不支持,所以只能用關聯類型來處理
protocol CarFactory<T> {
func createCar() -> T
}
// 也就是這種形式的協議泛型
protocol CarFactory {
associatedtype CarType
func createCar() -> CarType
}
六、類型擦除
前面5小節,都是鋪墊,是從實踐角度給出示例代碼一步步引出類型擦除。如果一時不理解上述各節所講內容,或者理念所說的內容與讀者的觀念有衝突,那麼暫時先放到一邊(不太影響對類型擦除的理解)。我們正式進入類型擦除的講解。
// 汽車工廠協議,此協議含有一個關聯類型
protocol CarFactory {
associatedtype CarType
func produce() -> CarType
}
// 電動車
struct ElectricCar {
let brand: String
}
// 汽油車
struct PetrolCar {
let brand: String
}
// Tesla汽車工廠
struct TeslaFactory: CarFactory {
typealias CarType = ElectricCar
func produce() -> TeslaFactory.CarType {
print("producing tesla electric car ...")
return ElectricCar(brand: "Tesla")
}
}
// 使用特斯拉汽車工廠
let teslaFactory = TeslaFactory()
teslaFactory.produce() // producing tesla electric car ...
// 增加寶馬汽車工廠
struct BMWFactory: CarFactory {
typealias CarType = ElectricCar
func produce() -> BMWFactory.CarType {
print("producing bmw electric car ...")
return ElectricCar(brand: "BMW")
}
}
// 增加豐田汽車工廠
struct ToyotaFactory: CarFactory {
typealias CarType = PetrolCar
func produce() -> ToyotaFactory.CarType {
print("producing toyota petrol car ...")
return PetrolCar(brand: "Toyota")
}
}
// 使用寶馬汽車工廠
let bmwFactory = BMWFactory()
bmwFactory.produce() // producing bmw electric car ...
// 使用豐田汽車工廠
let toyotaFactory = ToyotaFactory()
toyotaFactory.produce()// producing toyota petrol car ...
本示例的基礎代碼已完成,那麼假如:我們需要創建一個電動汽車工廠的數組要怎麼做?
let electricCarFactories: [CarFactory]
// Protocol 'CarFactory' can only be used as a generic constraint because it has Self or associated type requirements
// 編譯器抱怨說CarFactory有Self或者關聯類型,導致它不能創建數組,並且告訴我們此種協議類型只能用來做類型約束
其實編譯器是可以允許這麼做的,但是由於Swift的設計理念堅持要求類型安全(CarFactory還有關聯類型,此時關聯類型不確定,所以被認爲是類型不安全的),所以被禁止。
蘋果的可愛之處就是,堵了一條路,然後又開了一扇窗。繞過編譯器的這種限制的方法就是——類型擦除。類型擦除簡單理解就是:通過一個包裝類來規避類型上述問題的做法。ShowTime來了,睜大雙眼看看怎麼操作把。
struct AnyCarFactory<CarType>: CarFactory {
private let _produce: () -> CarType
//只接受實現了CarFactoryProtocol的實例對象
init<Factory: CarFactory>(_ carFactory: Factory) where Factory.CarType == CarType {
_produce = carFactory.produce
}
func produce() -> CarType {
return _produce()
}
}
上述代碼解釋如下:
- 第一行創建了AnyCarFactory類型,其是泛型類型(類型參數是CarType),且實現了CarFactory協議
- 第二行創建了一個成員變量,該變量是一個函數,其函數類型與CarFactory中的produce方法類型一致(這不是偶然的)
- 第五行聲明瞭一個初始化方法
- 該方法是一個模板方法,模板參數需實現CarFactory協議
- 該方法的模板參數的CarType需要與自身的CarType類型一致
- produce方法內部調用了成員變量來實現CarFactory的要求
大功告成了!秀代碼的時間到了。
// 此時數組的元素類型是AnyCarFactory<ElectricCar>
let factories = [AnyCarFactory(TeslaFactory()), AnyCarFactory(BMWFactory())]
factories.map() { $0.produce() }
// Output:
// producing tesla electric car ...
// producing bmw electric car ...
// 也許有讀者會提車這種形式的數組,這雖然能夠實現,但是,此種作坊正是Swift設計理念極力避免的——Any類型缺失,而,Swift強調類型安全
let factories: [Any] = [TeslaFactory(), BMWFactory()]
上述類型擦除是如何實現的呢?
- 首先,創建AnyCarFactory這個泛型類型
- 其次,泛型類型內部包含一個具體的,實現了汽車工廠協議的實際工廠(該工廠就是要被擦除類型的工廠)
- 再次,在AnyCarFactory初始化的時候,通過初始化參數,來獲取泛型的類型。
七、總結
如果看到這裏,讀者看懂了,那麼恭喜你!如果沒看懂,那麼不要氣餒,很可能是筆者沒有講好。那麼就要靠你自己多思考多研究了!
類型擦除在Swift標準庫裏也有實現,如AnySequence。
類型擦除作用有時很大,有時也不大,主要看應用場景。其需要了解要擦除類型的具體類型。
八、致謝
以下是筆者所閱讀過的相關網文,他們促進了筆者對類型擦除的理解。