Swift擴展的三個微妙細節
每當我初次翻看某文檔時,我都走馬觀花似的快速閱過,還一邊點著頭一邊喃喃自語說:“好!懂了,就這么回事!”,可是過后當我真正要運用到這些我以為已經理解了的知識點時,卻發現實際情況和我想的往往不一樣,每當這時我就懵了,心想:“哇哦…怎么回事?這和我想的完全不一樣啊!文檔里有說這事嗎?”。
最近的幾次討論促使我捫心自問是否真正的理解了Swift中的擴展。我閱讀過關于擴展的文檔,并且我“認為”我自己對這塊內容已經是理解的相當透徹了。可是這幾次討論,加上自己私下通過敲代碼的驗證,讓我發現了我原先不曾注意到幾個微妙的細節。
更新:這篇文章剛一發表,Swift社區就出手襄助并幫助我弄明白了我最根本的糾結點在哪。為此,我寫了另一篇文章“闡明Swift訪問控制”進一步說明我之前的誤解。為了避免犯我曾今犯過的錯誤,我建議大家去讀一讀。
三個關于擴展的微妙細節
對下面列出的三個細節的思考嚴重挑戰了我之前對Swift擴展的理解:
Swift擴展對它所擴展類型的visibility。比如,擴展能訪問被private所修飾的內容嗎?
定義擴展的位置是否對擴展的visibility有影響。比如我這有一個類型我想寫個擴展,把擴展寫在同一個源文件里和把擴展寫在另一個文件里有什么區別嗎?
擴展里“成員”的默認訪問修飾符以及是否給他們添加修飾對這個擴展作為一個類型的公共接口的影響。
在我開始之前,假設我有一個公共結構體Person。這個結構體有一些私有屬性,name,gender,和age。用一個枚舉把Gender封裝了一下。這個結構體看起來如下:
- public struct Person {
- private var name: String
- private var gender: Gender
- private var age: Int
- public init(name: String, gender: Gender, age: Int) {
- self.name = name
- self.gender = gender
- self.age = age
- }
- public func howOldArdYou() -> String {
- return formattedAge()
- }
- // 私有方法,用于下面分析擴展的`visibility`...
- private func formattedAge() -> String {
- switch self.gender {
- case .Male:
- return "I'm \(self.age)."
- case .Female:
- return "Not telling."
- }
- }
- public enum Gender {
- case Male
- case Female
- }
- }
現在,就讓我們給Person寫個擴展,通過實踐來弄清楚剛剛提到的三個小細節…
擴展對類型的訪問能力
當我提出***個細節時,關于擴展對被擴展類型的訪問能力時,我問了一個問題:“擴展能訪問到被private修飾的內容嗎?”。答案一開始出乎我的預料:能…擴展能訪問到。
然而,這里就要考慮到第二個細節所涉及的問題,那就是:在哪里定義這個擴展是絕對有影響的。
定義在同一個文件里
如果擴展和類型是在寫同一個源文件里,則擴展能訪問到在類型中被priavte所修飾的內容。
舉個栗子,在Person.swift里定義一個Person的擴展就會允許這個擴展訪問被private修飾的變量和方法
- extension Person {
- func getAge() -> Int {
- return age // 盡管age是 --private--, 但編譯成功
- }
- func getFormattedAge() -> String {
- return formattedAge() // 盡管 formattedAge是 --private--,但編譯成功
- }
- }
至于為什么把擴展寫在同一個源文件里頭會這樣,我自己的推理是其實可以在寫類型的時候,就把擴展的implementation當作類型的一部分給寫了,這樣的最終效果是一樣的。“這誰知道?!什么??為啥?”,我當時就沒想明白…
我在我要“擴展”的類型的源文件里,所以無論是我把要新添加的功能當作這個類型的擴展寫下來,或是就在這個類型里面定義我原本打算寫在擴展里的功能是沒有區別的,都是一樣的效果。
所以,站在編譯器的角度來看,編譯器可能會說:“好吧,我看到這里寫了一個擴展,但是真沒這個必要,因為擴展和類型都在同一個源文件里…,所以開發者完全可以把擴展里的這些代碼直接寫在類型里面…,所以他/她能夠訪問到被private修飾的代碼段。”
更新:我上面的寫的推理恰恰說明了我壓根就沒搞明白Swift訪問控制機制。所以我建議大家讀一讀我后來寫的“闡明Swfit控制機制”這篇文章,里面有更多的細節。
定義在不同文件里
把擴展寫在另一個文件里,則擴展無法訪問類型中那些被private修飾的內容了。
按照上文我自己推理的邏輯來反過來想,定義在不同文件中就訪問不了私有屬性對我來說也是說的通的。
大多數情況下,你都會給那些你沒有源代碼的類型擴展,在這種情況下,擴展就只能訪問那些被public修飾的內容了。
默認情況下的擴展訪問控制
對***一個細節的驗證也讓我更深的體會。蘋果官方文檔說了,但是直到我動手驗證了一番,我才算領會到了默認訪問控制修飾符給擴展所造成的微妙的影響。
沒有明確聲明訪問修飾賦時的默認訪問
簡單的說,當你聲明一個擴展但沒有特別明確指明訪問修飾符時(默認情況下),這個擴展的默認訪問等級取決于被擴展的那個類型的訪問等級。
* 如果類型是public或者是internal,那么擴展的implementation的“成員”就默認為internal。這里讓我沒想到是,除非你特別聲明,那么給public類型的擴展的成員變量在默認情況下也是internal。
* 如果類型是private,那么默認情況下擴展的implementation中的“成員”也是private
下面就是在我們不明確的聲明添加什么訪問修飾的前提下,來看擴展會是一個什么反應(為了能訪問私有屬性變量和方法,我在Person.swift里定義了這個擴展):
- public struct Person {
- // ...
- // ...
- }
- extension Person {
- func getAge() -> Int {
- return age
- }
- func getFormattedAge() -> String {
- return formattedAge()
- }
- }
同一模塊像上面這段代碼用默認的訪問修飾符時就會允許在同一個模塊中的實例訪問擴展里的API。但是,如果被擴展的類型的實例是在另一個模塊(比如在測試模塊),則無法訪問擴展中任何新增的公共API。
同一模塊
不同模塊(測試)
因為某些原因,我一直都以為如果給一個是public的類型添加擴展,那么擴展里的成員也理應是public。我不知道為什么我會這么想,但是幸好我的驗證把這點捋清楚了。
#p#
正常聲明擴展,但給擴展的implementation添加public修飾
給擴展的implementation的成員添加了public訪問控制修飾,那么不管是在同一模塊還是不同模塊(test target)都能訪問這些成員。
只要成員被public修飾,那么在同一個源文件里聲明擴展還是在另一個文件里聲明擴展已經無所謂了…但是,正如前文所講,只有在同一個源文件中聲明的擴展才能夠訪問那些被private修飾的類型成員變量。
在不同(左)和同一個(右)源文件中聲明的擴展
在不同模塊中也能訪問公共的擴展成員變量
這里請注意,在我寫extension Person {...}時,我沒有給這個擴展添加任何的修飾,我只是給這個擴展的成員添加了public。即便如此,新添加的方法仍然可以在不同的模塊中被訪問到。
也就是說,沒有必要寫public extension Person {...}。因為Person已經是public了,所以基于Person的擴展也就很自然的延用了類型本身的訪問等級。
總結
對我來說,這篇文章所提到的三個關于Swift擴展的細節已足以讓我敲敲代碼去驗證一番了。我希望這里所作的分析能夠為那些嘗試理解Swfit擴展的朋友掃清一些障礙。