這是我基于英文原文翻譯的譯文褥芒,如果你對本文感興趣而且想轉(zhuǎn)發(fā),你應(yīng)該在轉(zhuǎn)發(fā)文章里加上本文的鏈接
譯者:britzlieg
作為Swift中最重要的特性之一工扎,泛型使用起來很巧妙捉兴。很多人都不太能理解并使用泛型臭胜,特別是應(yīng)用開發(fā)者。泛型最適合libraries, frameworks, and SDKs的開發(fā)魂迄。在這篇文章中粗截,我將用不同于其他教程的角度來講解泛型。我們將使用餐館的例子捣炬,這個餐館能從SwiftCity的城市理事會中獲得授權(quán)熊昌。為了保持簡潔,我將內(nèi)容控制在以下四個主題:
- 1湿酸、泛型函數(shù)和泛型類型
- 2婿屹、關(guān)聯(lián)類型協(xié)議
- 3、泛型的Where語句
- 4推溃、泛型下標(biāo)
我們接下來看看具體怎么做昂利!
泛型函數(shù)和泛型類型
開一家Swift餐館
讓我們新開張一家餐館。當(dāng)開張的時候铁坎,我們不僅關(guān)注餐館的結(jié)構(gòu)蜂奸,也關(guān)注來自城市理事會的授權(quán)。更重要的硬萍,我們將關(guān)注我們的業(yè)務(wù)窝撵,以便于它功能化和有利可圖。首先襟铭,怎么讓一家公司怎么看上去像一個理事會碌奉?一個公司應(yīng)該要有一些基礎(chǔ)的功能。
protocol Company {
func buy(product: Product, money: Money)
func sell(product: Product.Type, money: Money) -> Product?
}
buy
函數(shù)把商品添加到庫存中寒砖,并花費(fèi)公司相應(yīng)的現(xiàn)金赐劣。sell
函數(shù)創(chuàng)建/查找所需花費(fèi)的該類型商品,并返回出售的商品哩都。
泛型函數(shù)
在這個協(xié)議中魁兼,Product
如果是一個確定的類型的話不太好。把每一個product
統(tǒng)一成一個確定的商品類型是不可能的漠嵌。每個商品都有自己的功能咐汞,屬性等盖呼。在這些各種類型的函數(shù)中,使用一個確定的類型是一個壞主意化撕。讓我們回到理事會那里看看几晤。總而言之植阴,不管是哪個公司蟹瘾,它都需要購買和賣出商品。所以掠手,理事會必須找到適合這兩個功能的一種通用的解決方案憾朴,以適合于每家公司。他們可以使用泛型來解決這個問題喷鸽。
protocol Company {
func buy<T>(product: T, with money: Money)
func sell<T>(product: T.Type, for money: Money) -> T?
}
我們把我們原來的確定類型Product
用默認(rèn)類型T
來代替众雷。這個類型參數(shù)<T>
把這些函數(shù)定義成泛型。在編譯時做祝,默認(rèn)類型會被確定類型替代砾省。當(dāng)buy和sell函數(shù)被調(diào)用時,具體類型就會被確定下來剖淀。這使得不同產(chǎn)品能靈活使用同一個函數(shù)纯蛾。例如,我們在Swift餐館中賣Penne Arrabiata纵隔。我們可以像下面一樣直接調(diào)用sell
函數(shù):
let penneArrabiata = swiftRestaurant.sell(product: PenneArrabiata.Self, for: Money(value:7.0, currency: .dollar))
在編譯時翻诉,編譯器用類型PenneArrabiata
替換類型T
。當(dāng)這個方法在運(yùn)行時被調(diào)用的時候捌刮,它已經(jīng)時有一個確定的類型PenneArrabiata
而不是一個默認(rèn)的類型碰煌。但這帶來另外一個問題,我們不能只是簡單的買賣各種類型的商品绅作,還要定義哪些商品時能夠被合法買賣芦圾。這里就引入where類型約束。理事會有另一個協(xié)議LegallyTradable
俄认。它將檢查和標(biāo)記我們可以合法買賣的商品个少。理事會強(qiáng)制我們對所有買賣實(shí)行這個協(xié)議,并列舉每一個符合協(xié)議的從商品眯杏。所以我們需要為我們的泛型函數(shù)添加約束夜焦,以限制只能買賣符合協(xié)議的商品。
protocol Company {
func buy<T: LegallyTradable>(product: T, with money: Money)
func sell<T: LegallyTradable>(product: T.Type, for money: Money) -> T?
}
現(xiàn)在岂贩,我們可以放心用這些函數(shù)了茫经。通常,我們把符合LegallyTradable
協(xié)議的默認(rèn)類型T
作為我們Company
協(xié)議函數(shù)的參數(shù)。這個約束被叫做Swift中的協(xié)議約束卸伞。如果一個商品不遵循這個協(xié)議抹镊,它將不能作為這個函數(shù)的參數(shù)。
泛型類型
我們把注意力轉(zhuǎn)移到我們的餐館上荤傲。我們得到授權(quán)并準(zhǔn)備關(guān)注餐館的管理垮耳。我們聘請了一位出色的經(jīng)理和她想建立一套能跟蹤商品庫存的系統(tǒng)。在我們的餐館中弃酌,我們有一個面食菜單氨菇,顧客喜歡各種各樣的面食儡炼。這就是我們?yōu)槭裁葱枰粋€很大的地方去存儲面食妓湘。我們創(chuàng)建一個面食套餐列表,當(dāng)顧客點(diǎn)套餐的時候乌询,將套餐從列表中移除榜贴。無論何時,餐館會買面食套餐妹田,并把它加到我們的列表中唬党。最后,如果列表中的套餐少于三個鬼佣,我們的經(jīng)理將訂新的套餐驶拱。這是我們的PastaPackageList
結(jié)構(gòu):
struct PastaPackageList {
var packages: [PastaPackage]
mutating func add(package: PastaPackage) {
packages.append(item)
}
mutating func remove() -> PastaPackage {
return packages.removeLast()
}
func isCapacityLow() -> Bool {
return packages.count < 3
}
}
過了一會,我們的經(jīng)理開始考慮為餐館中的每一樣商品創(chuàng)建一個列表晶衷,以便更好的跟蹤蓝纲。與其每次創(chuàng)建獨(dú)立列表結(jié)構(gòu),不如用泛型來避免這個問題晌纫。如果我們定義我們的庫存列表作為一個泛型類税迷,我們可以很容易使用同樣的結(jié)構(gòu)實(shí)現(xiàn)創(chuàng)建新的庫存列表。與泛型函數(shù)一樣锹漱,使用參數(shù)類型<T>
定義我們的結(jié)構(gòu)箭养。所以我們需要用T
默認(rèn)類型來替代PastaPackage
具體類型
struct InventoryList<T> {
var items: [T]
mutating func add(item: T) {
items.append(item)
}
mutating func remove() -> T {
return items.removeLast()
}
func isCapacityLow() -> Bool {
return items.count < 3
}
}
這些泛型類型讓我們可以為每個商品創(chuàng)建不同的庫存列表,而且使用一樣的實(shí)現(xiàn)哥牍。
var pastaInventory = InventoryList<PastaPackage>()
pastaInventory.add(item: PastaPackage())
var tomatoSauceInventory = InventoryList<TomatoSauce>()
var flourSackInventory = InventoryList<FlourSack>()
泛型的另外一個優(yōu)勢是只要我們的經(jīng)理需要額外的信息毕泌,例如庫存中的第一種商品,我們都可以通過使用擴(kuò)展來添加功能嗅辣。Swift允許我們?nèi)懡Y(jié)構(gòu)體撼泛,類和協(xié)議的擴(kuò)展。因?yàn)榉盒偷臄U(kuò)展性辩诞,當(dāng)我們定義結(jié)構(gòu)體時坎弯,不需要提供類型參數(shù)。在擴(kuò)展中,我們?nèi)匀挥媚J(rèn)類型抠忘。讓我們看看我們?nèi)绾螌?shí)現(xiàn)我們經(jīng)理的需求撩炊。
extension InventoryList { // We define it without any type parameters
var topItem: T? {
return items.last
}
}
InventoryList
中存在類型參數(shù)T
作為類型topItem
的遵循類型,而不需要再定義類型參數(shù)∑槁觯現(xiàn)在我們有所有商品的庫存列表拧咳。因?yàn)槊總€餐館都要從理事會中獲取授權(quán)去長時間存儲商品,我們依然沒有一個存儲的地方囚灼。所以骆膝,我們把我們的關(guān)注點(diǎn)放到理事會上。
關(guān)聯(lián)類型協(xié)議
我們再次回去到城市理事會去獲取存儲食物的允許灶体。理事會規(guī)定了一些我們必須遵守的規(guī)則阅签。例如,每家有倉庫的餐館都要自己清理自己的倉庫和把一些特定的食物彼此分開蝎抽。同樣政钟,理事會可以隨時檢查每間餐館的庫存。他們提供了每個倉庫都要遵循的協(xié)議樟结。這個協(xié)議不能針對特定的餐館养交,因?yàn)閭}庫物品可以改變成各種商品,并提供給餐館瓢宦。在Swift中碎连,泛型協(xié)議一般用關(guān)聯(lián)類型。讓我們看看理事會的倉庫協(xié)議是怎么樣的驮履。
protocol Storage {
associatedtype Item
var items: [Item] { set get }
mutating func add(item: Item)
var size: Int { get }
mutating func remove() -> Item
func showCurrentInventory() -> [Item]
}
Storage
協(xié)議并沒有規(guī)定物品怎么存儲和什么類型被允許存儲鱼辙。在所有商店,實(shí)現(xiàn)了Storage
協(xié)議的餐館必須制定一種他們他們存儲的特定類型的商品疲吸。這要保證物品從倉庫中添加和移除的正確性座每。同樣的,它必須能夠完整展示當(dāng)前倉庫摘悴。所以峭梳,對于我們的倉庫,我們的Storage
協(xié)議如下所示:
struct SwiftRestaurantStorage: Storage {
typealias Item = Food // Optional
var items = [Food]()
var size: Int { return 100 }
mutating func add(item: Food) { ... }
mutating func remove() -> Food { ... }
func showCurrentInventory() -> [Food] { ... }
}
我們實(shí)現(xiàn)理事會的Storage
協(xié)議□逵鳎現(xiàn)在看來葱椭,關(guān)聯(lián)類型Item
可以用我們的Food
類型來替換。我們的餐館倉庫都可以存儲Food
口四。關(guān)聯(lián)類型Item
只是一個協(xié)議的默認(rèn)類型孵运。我們用typealias
關(guān)鍵字來定義類型。但是蔓彩,需要指出的是治笨,這個關(guān)鍵字在Swift中是可選的驳概。即使我們不用typealias
關(guān)鍵字,我們依然可以用Food
替換協(xié)議中所有用到Item
的地方旷赖。Swift會自動處理這個顺又。
限制關(guān)聯(lián)類型為特定類型
事實(shí)上,理事會總是會想出一些新的規(guī)則并強(qiáng)制你去遵守等孵。一會后稚照,理事會改變了Storage
協(xié)議。他們宣布他們將不允許任何物品在Storage
俯萌。所有物品必須遵循StorableItem協(xié)議果录,以保證他們都適合存儲。換句話咐熙,它們都限制為關(guān)聯(lián)類型Item
protocol Storage {
associatedtype Item: StorableItem // Constrained associated type
var items: [Item] { set get }
var size: Int { get }
mutating func add(item: Item)
mutating func remove() -> Item
func showCurrentInventory() -> [Item]
}
用這個方法弱恒,理事會限制類型為當(dāng)前關(guān)聯(lián)類型。任何實(shí)現(xiàn)Storage
協(xié)議的都必須使用實(shí)現(xiàn)StorableItem
協(xié)議的類型糖声。
泛型的Where語句
使用泛型的Where語句的泛型
讓我們回到文章剛開始的時候斤彼,看看Company
協(xié)議中的Money
類型分瘦。當(dāng)我們討論到協(xié)議時蘸泻,買賣中的money參數(shù)事實(shí)上是一個協(xié)議。
protocol Money {
associatedtype Currency
var currency: Currency { get }
var amount: Float { get }
func sum<M: Money>(with money: M) -> M where M.Currency == Currency
}
然后嘲玫,再過了一會悦施,理事會打回了這個協(xié)議,因?yàn)樗麄冇辛硪粋€規(guī)則去团。從現(xiàn)在開始抡诞,交易只能用一些特定的貨幣。在這個之前土陪,我們能各種用Money
類型的貨幣昼汗。不同于每種貨幣定義money類型的做法,他們決定用Money
協(xié)議來改變他們的買賣函數(shù)鬼雀。
protocol Company {
func buy<T: LegallyTradable, M: Money>(product: T.Type, with money: M) -> T? where M.Currency: TradeCurrency
func sell<T: LegallyTradable, M: Money>(product: T, for money: M) where M.Currency: TradeCurrency
}
where語句和類型約束的where語句的區(qū)別在于顷窒,where語句會被用于定義關(guān)聯(lián)類型。換句話源哩,在協(xié)議中鞋吉,我們不能限制關(guān)聯(lián)的類型,而會在使用協(xié)議的時候限制它励烦。
泛型的where語句的擴(kuò)展
泛型的where語句在擴(kuò)展中有其他用法谓着。例如,當(dāng)理事會要求用漂亮的格式(例如“xxx EUR”)打印money時坛掠,他們只需要添加一個Money
的擴(kuò)展赊锚,并把Currency
限制設(shè)置成```Euro``治筒。
extension Money where Currency == Euro {
func printAmount() {
print("\(amount) EUR")
}
}
泛型的where語句允許我們添加一個新的必要條件到Money
擴(kuò)展中,因此只有當(dāng)Currency
是Euro
時舷蒲,擴(kuò)展才會添加printAmount()
方法矢炼。
泛型的where 語句的關(guān)聯(lián)類型
在上文中,理事會給Storage
協(xié)議做了一些改進(jìn)阿纤。當(dāng)他們想檢查一切是否安好句灌,他們想列出每一樣物品,并控制他們欠拾∫刃浚控制進(jìn)程對于每個Item
是不一樣的。因?yàn)檫@樣藐窄,理事會僅僅需要提供Iterator
關(guān)聯(lián)類型到Storage
協(xié)議中资昧。
protocol Storage {
associatedtype Item: StorableItem
var items: [Item] { set get }
var size: Int { get }
mutating func add(item: Item)
mutating func remove() -> Item
func showCurrentInventory() -> [Item]
associatedtype Iterator: IteratorProtocol where Iterator.Element == Item
func makeIterator() -> Iterator
}
Iterator
協(xié)議有一個叫Element``的關(guān)聯(lián)類型。在這里荆忍,我們給它加上一個必要條件格带,在
Storage協(xié)議中,
Element必須與
Item```類型相等刹枉。
泛型下標(biāo)
來自經(jīng)理和理事會的需求看起來是無窮無盡的叽唱。同樣的,我們需要滿足他們的要求微宝。我們的經(jīng)理跑過來跟我們說她想要用一個Sequence
來訪問存儲的物品棺亭,而不需要訪問所有的物品。經(jīng)理想要個語法糖蟋软。
extension Storage {
subscript<Indices: Sequence>(indices: Indices) -> [Item] where Indices.Iterator.Element == Int {
var result = [Item]()
for index in indices {
result.append(self.items[index])
}
return result
}
}
在Swift 4中镶摘,下標(biāo)也可以是泛型,我們可以用條件泛型來實(shí)現(xiàn)岳守。在我們的使用中凄敢,indices
參數(shù)必須實(shí)現(xiàn)Sequence
協(xié)議。從Apple doc中可以知道湿痢,“The generic where
clause requires that the iterator for the sequence must traverse over elements of type Int
.”這就保證了在sequence的indices跟存儲中的indices是一致的涝缝。
結(jié)語
我們讓我們的餐館功能完備。我們的經(jīng)理和理事會看起來也很高興蒙袍。正如我們在文章中看到的俊卤,泛型是很強(qiáng)大的。我們可以用泛型來滿足各種敏感的需求害幅,只要我們知道概念消恍。泛型在Swift的標(biāo)準(zhǔn)庫中也應(yīng)用廣泛。例如以现,Array 和 Dictionary類型都是泛型集合狠怨。如果你想知道更多约啊,你可以看看這些類是怎么實(shí)現(xiàn)的。 Swift Language Doc 也提供了泛型的解析佣赖。最近Swift語言提供了泛型的一些說明Generic Manifesto恰矩。我建議你去看完所有的文檔,以便更好的理解當(dāng)前用法和未來的規(guī)劃憎蛤。感謝大家的閱讀外傅!如果你對接下來的文章有疑惑,建議俩檬,評論或者是想法萎胰,清在 Twitter 聯(lián)系我,或者評論棚辽!你也可以在GitHub上關(guān)注我哦技竟!
本文Github地址