- 原文作者:Pablo Villar
- 译文出自:掘金翻譯計劃
- 译者:Zheaoli
- 校對者:Kulbear, Tuccuay
昨天,我開始將這個 Jayme 遷移到 Swift 3。這是我第一次將一個項目從 Swift 2.2 遷移至 Swift 3。說實話這個過程十分的繁瑣,由於 Swift 3 在舊版本基礎上發生了很多比較大的改變,我不得不承認眼前這樣一個事實,除了花費較多的時間以外,沒有其餘的捷徑可走。不過這樣的經歷也帶來一點好處:我對 Swift 3 的理解變得更為深入,對我來講,這可能是最好的消息了。😃
在遷移代碼的過程中,我需要做出很多的選擇。更為蛋疼的是,整個遷移過程並不是修改代碼那麼簡單,你還需要用耐心去一點點適應 Swift 3 中帶來的新變化。某種意義上來講,修改代碼只是整個遷移過程的開始而已。
如果你已經決定將你的代碼遷移到 Swift 3,我建議你去看看這篇文章來作為你萬里長征的第一步。
如果一切順利的話,在不久以後,我將回去寫一篇博客來記錄下整個遷移過程中的點點滴滴,包括我所作出的決定等等。但是眼前,我將會把注意力集中在一個非常非常重要的問題上:怎樣正確的編寫函數簽名.
開篇#
首先,讓我們來看看在 Swift 3 與 Swift 2 相比函數命名方式的差異吧。
在 Swift 2 中,函數中的第一個參數的標籤在調用時可以省略,這是為了遵循這樣一個 good ol' Objective-C conventions 標準。比如我們可以這樣寫代碼:
// Swift 2
func handleError(error: NSError) { }
let error = NSError()
handleError(error) // Looks like Objective-C
在 Swift 3 中調用函數時,其實也是有辦法省略第一個參數的標籤的,但默認情況下不是這樣:
// Swift 3
func handleError(error: NSError) { }
let error = NSError()
handleError(error) // Does not compile!
// ⛔ Missing argument label 'error:' in call
當遇到這樣的情況時,我們第一反應可能是下面這樣的:
// Swift 3
func handleError(error: NSError) { }
let error = NSError()
handleError(error: error)
// Had to write 'error' three times in a row!
// My eyes already hurt 🙈
當然如果這樣做,你肯定會很快意識到你的代碼將會變得有多坑爹。
如同前面所說的一樣,在 Swift 3 中,我們是可以在調用函數時,將第一個參數的標籤省略的,但是記住,你要去明確的告訴編譯器這一點:
// Swift 3
func handleError(_ error: NSError) { }
// 🖐 Notice the underscore!
let error = NSError()
handleError(error) // Same as in Swift 2
你可能在使用 Xcode 自帶的遷移工具進行遷移時遇到這樣的情況。
注意,在函數簽名中的下劃線的意思是:告訴編譯器,我們在調用函數時第一個參數不需要外帶標籤。這樣,我們可以按照 Swift 2 中的方式去調用函數。
此外,你需要意識到,Swift 3 之所以修改了函數編寫方式,是為了保證其一致性與可讀性:我們不再需要對不同的參數區別對待。我想這可能是你遇到的第一個問題。
好了,現在代碼可以編譯運行了,但是你必須知道,你需要反復的去閱讀 Swift 3 API design guidelines 一文。
☝️ 一點微小的人生經驗:你需要隨時去誦讀 Swift 3 API design guidelines 一文,這會為你解鎖 Swift 開發的新體位。
第二步,精簡你的代碼#
讓我們再來看看之前的代碼:
為了精簡我們的代碼,你可以將你的代碼進行修剪一番,比如去除函數名裡的類型信息等。
// Swift 3
func handle(_ error: NSError) { /* ... */ }
let error = NSError()
handle(error) // Type name has been pruned
// from function name, since it was redundant
如果你想讓你的代碼變得更短,更精悍,更明瞭的話,我給你們講,作為一個欽定的開發者,一定要去反復誦讀這篇 Swift 3 API design guidelines 文章到可以默寫為止。
要注意讓函數的調用過程是清晰、明確的,我們根據以下兩點來確定函數的的命名和參數:
- 我們知道函數的返回類型
- 我們知道參數所對應的類型(比如在上面這個例子中,我們毫無疑問的知道其參數所屬的類型是 NSError)。
更多的一些問題#
現在請睜大眼睛看清楚我們下面所討論的東西。 ⚠️
上面我們所講的東西並沒有包括所有可能出現的情況,換句話說,你可能遇到這樣一種特殊情況,即,一個參數的類型沒有辦法直觀的體現其作用。
讓我們考慮下面這樣一種情況:
// Swift 2
func requestForPath(path: String) -> URLRequest { }
let request = requestForPath("local:80/users")
如果你想將代碼遷移到 Swift 3,那麼根據已有的知識,你可能會這麼做:
// Swift 3
func request(_ path: String) -> URLRequest { }
let request = request("local:80/users")
講真,這段代碼看起來可讀性很差,讓我們稍微修改下:
// Swift 3
func request(for path: String) -> URLRequest { }
let request = request(for: "local:80/users")
OK,現在看起來舒服多了,但是並沒有解決我上面提到的問題。
在我們調用這個函數的時候,我們怎樣很直觀的知道我們需要給這個參數傳遞一個 Web Url 呢?你所能提前知道的是你需要傳遞一個 String 類型的變量進去,但是你並不清楚你需要傳遞一個 Web Url 進去。
同理,我們在一個大型項目中,我們需要很清楚的明白每個參數的作用所在,但是很明顯,目前我們還沒有解決這個大問題,比如:
- 你怎麼知道一個
String
類型的變量代表著 Web Url。 - 你怎麼知道一個
Int
類型的變量代表著 Http 狀態碼。[String: String]
- 你怎麼知道一個
[String: String]
類型的變量代表著 Http Header。 - 等等...。
⚠️ 综上,我給你們一點微小的人生經驗吧: 謹慎精簡你的代碼 ✄
回到代碼上,我們可以給參數添加上相對應的標籤來解決這個問題,好了看看下面這個代碼:
func request(forPath path: String) -> URLRequest { }
let request = request(forPath: "local:80/users")
好了,現在代碼看起來是不是更清楚,可讀性更強了呢? 🎉 恭喜~
講真,看到這裡其實你可以關閉瀏覽器了,但是事實上,下面才是最精華的部分。
好了,讓我們來看看關於函數參命名的用詞問題:
func request(forPath path: String) -> URLRequest { }
// The word 'path' appears twice
這段代碼看起來不錯,但是如果你想讓其變得更好,那麼請看接下來的部分。
你所不知道的小技巧#
這個小技巧很簡單:在上下文中反映參數的類型及作用,這樣你就可以無腦的精簡你的代碼了。
呐,我們來看看下面這段代碼。
typealias Path = String // To the rescue!
func request(for path: Path) -> URLRequest { }
let request = request(for: "local:80/users")
在這個例子中,參數的類型和參數的作用表達達成了完美的統一,因為你在上下文中為 String
賦予了一個別名叫做 Path
。
現在,你的函數看起來還是依舊的精簡,可讀性較高,但是卻不重複。
以此類推,你可以使用同樣的方式來書寫一些優美的代碼,比如:
typealias Path = String
typealias StatusCode = Int
typealias HTTPHeader = [String: String]
// etc...
如你所見,你可以盡情的寫精簡而優美的代碼了。
不過,請記住,凡事走向極端便變了味了:這個小技巧會為你的代碼添加額外的負擔,特別是你們代碼存在多重嵌套的情況。因此請記住,如果你無腦的使用這樣的小技巧的話,那麼你可能會付出一些慘痛的代價。
結論#
很多時候,你在使用 Swift 3 時,命名函數的時候你會遇到很多困難。
積累一些代碼片段可能會幫助你很多:
func remove(at position: Index) -> Element { }
employees.remove(at: x)
func remove(_ member: Element) -> Element? { }
allViews.remove(cancelButton)
func url(forPath path: String) -> URL { }
let url = url(forPath: "local:80/users")
typealias Path = String // Alternative
func url(for path: Path) -> URL { }
let url = url(for: "local:80/users")
func entity(from dictionary: [String: Any]) -> Entity { /* ... */ }
let entity = entity(from: ["id": "1", "name": "John"])