Swift 輕松掌握喵神高階心法 V
過去的一個月將一個H5項目從0-1給搞出來了,說實話Vue,React, Angular, 三足鼎立的態勢下,選擇組件化的Vue.js是明智之舉~ 上完UAT就能夠有時間繼續我的Swift3.0的學習啦,哈哈,好久沒有敲Swift竟然感覺到有些生疏。。
Tip18 Designated,Convenience 和 Required
我們在深入初始化方法之前,不妨先再想想 Swift 中的初始化想要達到一種怎樣的目的。
其實就是安全。在 Objective-C 中,init 方法是非常不安全的:沒有人能保證 init 只被調用一次,也沒有人保證在初始化方法調用以后實例的各個變量都完成初始化,甚至如果在初始化里使用屬性進行設置的話,還可能會造成各種問題,雖然 Apple 也明確說明了不應該在 init 中使用屬性來訪問,但是這并不是編譯器強制的,因此還是會有很多開發者犯這樣的錯誤。
所以 Swift 有了超級嚴格的初始化方法。一方面,Swift 強化了 designated 初始化方法的地位。Swift 中不加修飾的 init 方法都需要在方法中保證所有非 Optional 的實例變量被賦值初始化,而在子類中也強制 (顯式或者隱式地) 調用 super 版本的 designated 初始化,所以無論如何走何種路徑,被初始化的對象總是可以完成完整的初始化的。
class ClassA {
let numA: Int
init(num: Int) {
numA = num
}
}
class ClassB: ClassA {
let numB: Int
override init(num: Int) {
numB = num + 1
super.init(num: num)
}
}
在上面的示例代碼中,注意在 init 里我們可以對 let 的實例常量進行賦值,這是初始化方法的重要特點。在 Swift 中 let 聲明的值是常量,無法被寫入賦值,這對于構建線程安全的 API 十分有用。而因為 Swift 的 init 只可能被調用一次,因此在 init 中我們可以為常量進行賦值,而不會引起任何線程安全的問題。
與 designated 初始化方法對應的是在 init 前加上 convenience 關鍵字的初始化方法。這類方法是 Swift 初始化方法中的 “二等公民”,只作為補充和提供使用上的方便。所有的 convenience 初始化方法都必須調用同一個類中的 designated 初始化完成設置,另外 convenience 的初始化方法是不能被子類重寫或者是從子類中以 super 的方式被調用的。
class ClassA {
let numA: Int
init(num: Int) {
numA = num
}
convenience init(bigNum: Bool) {
self.init(num: bigNum ? 10000 : 1)
}
}
class ClassB: ClassA {
let numB: Int
override init(num: Int) {
numB = num + 1
super.init(num: num)
}
}
只要在子類中實現重寫了父類 convenience 方法所需要的 init 方法的話,我們在子類中就也可以使用父類的 convenience 初始化方法了。比如在上面的代碼中,我們在 ClassB 里實現了 init(num: Int) 的重寫。這樣,即使在 ClassB 中沒有 bigNum 版本的 convenience init(bigNum: Bool),我們仍然還是可以用這個方法來完成子類初始化:
let anObj = ClassB(bigNum: true)
anObj.numA //10000
anObj.numB //10001
因此進行一下總結,可以看到初始化方法永遠遵循以下兩個原則:
- 初始化路徑必須保證對象完全初始化,這可以通過調用本類型的 designated 初始化方法來得到保證;
- 子類的 designated 初始化方法必須調用父類的 designated 方法,以保證父類也完成初始化。
對于某些我們希望子類中一定實現的 designated 初始化方法,我們可以通過添加 required 關鍵字進行限制,強制子類對這個方法重寫實現。這樣做的最大的好處是可以保證依賴于某個 designated 初始化方法的 convenience 一直可以被使用。一個現成的例子就是上面的 init(bigNum: Bool):如果我們希望這個初始化方法對于子類一定可用,那么應當將 init(num: Int) 聲明為必須,這樣我們在子類中調用 init(bigNum: Bool) 時就始終能夠找到一條完全初始化的路徑了:
class ClassA {
let numA: Int
required init(num:Int) {
numA = num
}
convenience init(bigNum: Bool) {
self.init(num: bigNum ? 10000 : 1)
}
}
class ClassB:ClassA {
let numB: Int
required init(num: Int) {
numB = num + 1
super.init(num: num)
}
}
另外需要說明的是,其實不僅僅是對 designated 初始化方法,對于 convenience 的初始化方法,我們也可以加上 required 以確保子類對其進行實現。這在要求子類不直接使用父類中的 convenience 初始化方法時會非常有幫助。
Tip19 初始化返回nil
在 Objective-C 中,init 方法除了返回 self 以外,其實和一個普通的實例方法并沒有太大區別。如果你喜歡的話,甚至可以多次進行調用,這都沒有限制。一般來說,我們還會在初始化失敗 (比如輸入不滿足要求無法正確初始化) 的時候返回 nil 來通知調用者這次初始化沒有正確完成。
但是,在 Swift 中默認情況下初始化方法是不能寫 return 語句來返回值的,也就是說我們沒有機會初始化一個 Optional 的值。一個很典型的例子就是初始化一個 url。在 Objective-C 中,如果我們使用一個錯誤的字符串來初始化一個 NSURL 對象時,返回會是 nil 代表初始化失敗。所以下面這種 "防止度娘吞鏈接" 式的字符串 (注意兩個 t 之間的空格和中文的句號) 的話,也是可以正常編譯和運行的,只是結果是個 nil:
NSURL *url = [[NSURL alloc] initWithString:@"https://github.com/coderZsq"];
NSLog(@"%@",url);
//輸出(null)
但是在Swift中情況就不那么樂觀了, -initWithString: 在Swift中對應的是一個convenience init 方法: init(string URLString:String!).上面的Objective-C代碼在Swift中等效為:
let url = NSURL(string: "https://github.com/coderZsq")
print(url)
init 方法在Swift1.1中發生了很大的變化, 為了將來龍去脈講述清楚, 我們先來看看在Swift1.0下的表現:
Swift1.0及之前
如果在Swift1.0的環境下嘗試運行上面代碼的話, 我們會得到一個EXC_BAD_INSTRUCTION ,這說明觸發了Swift內部的斷言, 這個初始化方法不接受這樣的輸入. 一個常見的解決方法是使用工廠模式, 也就是寫一個類方法來生成和返回實例, 或者在失敗的時候返回nil. Swift的NSURL就做了這樣的處理:
class func URLWithString(URLString:String!)->Self!
使用的時候:
let url = NSURL.URLWithString("https://github.com/coderZsq")
print(url)
//輸出nil
不過雖然可以用這種方式來和原來一樣返回nil, 但是這也算事一種折中. 在可能的情況下, 我們還是應該傾向于盡量減少出現Optional的可能性,這樣更有助于代碼的簡化.
如果你確實想使用初始化方法而不愿意用工廠函數的話, 也可以考慮用一個Optional量來儲存結果
這樣你就可以處理初始化失敗了, 不過相應的代價是代碼復雜度的增加
let url: NSURL? = NSURL(string: "https://github.com/coderZsq")
// nil
Swift1.1及之后
雖然在默認情況下不能在init中返回nil, 但是通過上面的例子我們可以看到Apple自家的API還是有這個能力的.
好消息是在Swift1.1中Apple已經為我們加上初始化方法中返回nil的能力. 我們可以在init聲明時在其后加上一個 ? 或者 ! 來表示初始化失敗可能返回nil. 比如為Int添加一個接收String作為參數的初始化方法. 我們希望在方法中對中文和英文的數據進行解析, 并輸出Int結果, 對其解析并初始化的時候, 就可能遇到初始化失敗的情況:
extension Int {
init?(fromString: String) {
self = 0
var digit = fromString.characters.count - 1
for c in fromString.characters {
var number = 0
if let n = Int(String(c)) {
number = n
} else {
switch c {
case "一": number = 1
case "二": number = 2
case "三": number = 3
case "四": number = 4
case "五": number = 5
case "六": number = 6
case "七": number = 7
case "八": number = 8
case "九": number = 9
case "零": number = 0
default: return nil
}
}
self = self + number * Int(pow(10, Double(digit)))
digit = digit - 1
}
}
}
let number1 = Int(fromString: "12") //12
let number2 = Int(fromString: "三二五") //325
let number3 = Int(fromString: "七9八") //798
let number4 = Int(fromString: "吃了嗎") //nil
let number5 = Int(fromString: "la4n") //nil
所有的結果都將是Int?類型, 通過Optional Binding, 我們就能知道初始化是否成功, 并安全地使用它們了. 我們在這類初始化方法中還可以對self進行賦值, 也算是init方法里的特權之一.
同時像上面例子中的NSURL.URLWithString 這樣的工廠方法, 在Swift1.1中已經不再需要. 為了簡化API和安全, Apple已經被標記為不可用了并無法編譯. 而對應地, 可能返回nil的init方法都加上了?標記:
convenince init?(String URLString: String)
在新版本的Swift中, 對于可能初始化失敗的情況, 我們應該始終使用可返回nil的初始化方法, 而不是類型工廠方法.
Tip20 static 和 class
Swift中表示"類型范圍作用域"這一概念有兩個不同的關鍵字, 它們分別是static和class. 這兩個關鍵字確實都表達了這個意思, 但是在其他的一些語言, 包括Objective-C中, 我們并不會特別地區分類變量/類方法和靜態變量/靜態函數.但是在Swift的早期版本中, 這兩個關鍵字確實不能用混的.
在非 class 的類型上下文中, 我們統一使用static來表述類型作用域. 這包括在enum和struct中表述類型方法和類型屬性時. 在這兩個值類型中, 我們可以在類型范圍內聲明并使用存儲屬性, 計算屬性和方法.static適用的場景有這些:
struct Point {
let x: Double
let y: Double
//存儲屬性
static let zero = Point(x: 0, y: 0)
//計算屬性
static var ones: [Point] {
return [Point(x: 1,y: 1),
Point(x: -1,y: 1),
Point(x: 1,y: -1),
Point(x: -1,y: -1)]
}
//類型方法
static func add(p1: Point,p2: Point) -> Point {
return Point(x: p1.x + p2.x, y: p1.y + p2.y)
}
}
enum 的情況與這個十分類似, 就不再列舉了.
class 關鍵字相比起來就明白許多, 是專門用在class類型的上下文中的, 可以用來修飾類方法以及類的計算屬性. 但是有一個例外, class中現在是不能出現class的存儲屬性的, 我們如果寫類似這樣的代碼的話:
class MyClass {
class var bar: Bar?
}
編譯時會得到一個錯誤:
class varibales not yet supported
在Swift 1.2 及之后, 我們可以在class中使用static來聲明一個類作用域的變量. 也即:
class MyClass {
static var bar: Bar?
}
的寫法是合法的. 有了這個特性之后, 像單例的寫法就可以回歸我們所習慣的方式了.
有一個比較特殊的是protocol. 在Swift中class, struct和enum都是可以實現某個protocol的. 那么如果我們想在protocol里定義一個類型域上的方法或者計算屬性的話, 應該用哪個關鍵字呢? 答案是使用static進行定義. 在使用的時候, struct或enum中仍然使用static, 而在class里我們既可以使用class關鍵字, 也可以用static, 它們的結果是相同的:
protocol MyProtocol {
static func foo() -> String
}
struct MyStruct: MyProtocol {
static func foo() -> String {
return "MyStruct"
}
}
enum MyEnum: MyProtocol {
static func foo() -> String {
return "MyEnum"
}
}
class MyClass: MyProtocol {
//在class中可以使用class
class func foo() -> String {
return "MyClass.foo()"
}
//也可以使用static
static func bar() -> String {
return "MyClass.bar()"
}
}
在Swift1.2之前protocol中使用的是class作為關鍵字, 但著確實是不合邏輯的. Swift1.2和2.0分兩次對此進行了改進. 現在只需要記住結論, 在任何時候使用static應該都是沒有問題的.
Tip21 多類型和容器
Swift中常用的原生容器類型有三種, 他們分別是Array, Dictionay和Set:
struct Array<Element> : RandomAccessCollection, MutableCollection {
//...
}
struct Dictionary<Key : Hashable, Value> : Collection, ExpressibleByDictionaryLiteral {
//...
}
struct Set<Element : Hashable> : SetAlgebra, Hashable, Collection, ExpressibleByArrayLiteral {
//...
}
它們都是泛型的, 也就是說我們在一個集合中只能放同一個類型的元素. 比如:
let numbers = [1,2,3,4,5]
// numbers的類型是[Int]
let strings = ["hello","world"]
// string的類型是[String]
如果我們要吧不相關的類型放到同一個容器類型中的話, 需要做一些轉換的工作:
// Any類型可以隱式轉換
let mixed: [Any] = [1, "two", 3]
// 轉換為[NSObject]
let objectArray = [1 as NSObject, "two" as NSObject, 3 as NSObject]
這樣的轉換會造成部分信息的損失, 我們從容器中取值時只能得到信息完全丟失后的結果, 在使用時還需要進行一次類型轉換. 這其實是在無其他可選方案后的最差選擇: 因為使用這樣的轉換的話, 編譯器就不能再給我們提供警告信息了. 我們可以隨意地將任意對象添加進容器, 也可以將容器中取出的值轉換為任意類型, 這是一件十分危險的事情:
let any = mixed[0] // Any類型
let nsObject = objectArray[0] // NSObject類型
其實我們注意到, Any其實不是具體的某個類型. 因此就是說其實在容器類型泛型的幫助下, 我們不僅可以在容器中添加同一具體類型的對象, 也可以添加實現了同一協議的類型的對象. 絕大多數情況下, 我們想要放入一個容器中的元素或多或少會有某些共同點, 這就使得用協議來規定容器類型會很有用. 比如上面的例子如果我們希望的是打印出容器內的元素的description, 可能我們更傾向于將數組聲明為[CustomStringConvertible]的:
let mixed: [CustomStringConvertible] = [1, "two", 3]
for obj in mixed {
print(obj.description)
}
這種方法雖然也損失了一部分類型信息, 但是對于Any或者AnyObject還是改善很多, 在對于對象中存在某種共同特性的情況下無疑是最方便的. 另一種做法是使用enum可以帶有值的特點, 將類型信息封裝到特定的enum中. 下面的代碼封裝了Int或者String類型:
enum IntOrString {
case IntValue(Int)
case StringValue(String)
}
let mixed = [IntOrString.IntValue(1), IntOrString.StringValue("two"), IntOrString.IntValue(3)]
for value in mixed {
switch value {
case let .IntValue(i):
print(i * 2)
case let .StringValue(s):
print(s.capitalized)
}
}
通過這種方法, 我們完整地在編譯時保留了不同類型的信息. 為了方便, 我們甚至可以進一步為IntOrString使用字面量轉換的方法編寫簡單的獲取方式, 但那時另外一個故事了.
Tip22 default 參數
Swift的方法是支持默認參數的, 也就是說在聲明方法時, 可以給某個參數指定一個默認使用的值. 在調用該方法時要是傳入了這個參數, 則使用傳入的值. 如果缺少這個輸入參數, 那么直接使用設定的默認值進行調用. 可以說這是Objective社區盼了好多年的一個特性了, Objective-C由于語法的特點幾乎無法再不大幅改動的情況下很好地實現默認參數.
和其他很多語言的默認參數相比較, Swift中的默認參數限制更少, 并沒有所謂"默認參數之后不能再出現無默認值的參數"這樣的規則, 舉個例子, 下面兩種方法的聲明在Swift里都是合法可用的:
func sayHello1(str1: String = "Hello", str2: String, str3: String) {
print(str1 + str2 + str3)
}
func sayHello2(str1: String, str2: String, str3: String = "World") {
print(str1 + str2 + str3)
}
其他不少語言只能使用后面一種寫法, 將默認參數作為方法的最后一個參數.
在調用的時候, 我們如果想要使用默認值的話, 只要不傳入相應的值就可以了. 下面這樣的調用將得到同樣的結果:
sayHello1(str2: " ", str3: "World")
sayHello2(str1: "Hello", str2: " ")
// 輸出都是Hello World
這兩個調用都省略了帶有默認值的參數, sayHello1中str1是默認的"Hello", 而sayHello中的str3是默認的"World".
另外如果喜歡Cmd + 單擊點來點去到處看的朋友可能會注意到NSLocalizedString這個常用方法的簽名現在是:
func NSLocalizedString(_ key: String, tableName: String? = default, bundle: Bundle = default, value: String = default, comment: String) -> String
默認參數寫的是defualt, 這是含有默認參數的方法所產生的Swift的調用接口. 當我們指定一個編譯時就能確定的常量來作為默認參數的取值時, 這個取值時隱藏在方法實現內部, 而不應該暴露給其他部分. 與NSLocalizedString很相似的還有Swift中的各類斷言:
func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line)
Tip23 正則表達式
作為一門先進的編程語言, Swift可以說吸收了眾多其他先進語言的優點, 但是有一點確實讓人略微失望的, 就是Swift至今為止并沒有在語言層面上支持正則表達式.
大概是因為其實app開發并不像Perl或者Ruby那樣的語言需要處理很多文字匹配的問題, Cocoa開發者確實不是特別依賴正則表達式. 但是并不排除有希望使用正則表達式的場景, 我們是否能像其他語言一樣, 使用比如 =~ 這樣的符號來進行正則匹配呢?
最容易想到也是最容易實現的當然是自定義 =~ 運算符. 在Cocoa中我們可以使用NSRegularExpression 來做正則匹配, 那么其實我們為它寫一個包裝并不是什么太困難的事情. 因為做的字符串正則匹配, 所以 =~ 左右兩邊都是字符串. 我們可以先寫一個接受正則表達式的字符串, 以此生成NSRegularExpression對象. 然后使用該對象李艾匹配輸入字符串, 并返回結果告訴調用者匹配是否成功. 一個最簡單的實現可能是下面這樣的:
struct RegexHelper {
let regex: NSRegularExpression
init(_ pattern: String) throws {
try regex = NSRegularExpression(pattern: pattern, options: .caseInsensitive)
}
func match(_ input: String) -> Bool {
let matches = regex.matches(in: input, options: [], range: NSMakeRange(0, input.utf16.count))
return matches.count > 0
}
}
在使用的時候, 比如我們想要匹配一個郵箱地址, 我們可以這樣來使用:
let mailPattern = "^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$"
let matcher: RegexHelper
do {
matcher = try RegexHelper(mailPattern)
}
let maybeMailAddress = "https://github.com/coderZsq"
if matcher.match(maybeMailAddress) {
print("有效的郵箱地址")
}
// 輸出:
// 有效的郵箱地址”
現在我們有了方便的封裝, 接下來就讓我們實現 =~ 吧. 這里只給出結果了, 關于如何實現操作符和重載操作符的內容, 可以參考操作符一節的內容.
precedencegroup MatchPrecedence {
associativity: none
higherThan: DefaultPrecedence
}
infix operator =~: MatchPrecedence
func =~ (lhs: String, rhs: String) -> Bool {
do {
return try RegexHelper(rhs).match(lhs)
} catch _ {
return false
}
}
這下我們就可以使用類似于其他語言的正則匹配的方法了:
if "https://github.com/coderZsq" =~ "^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$"{
print("有效的郵箱地址")
}
Swift1.0版本主要會專注于成為一個非常適合制作app的語言, 而現在看來Apple和Chris也有野心將Swift帶到更廣闊的平臺去. 那時候可能會在語言層面加上正則表達式是支持, 到時候這篇tip可能也就沒有意義了, 不會我個人還是非常期盼那一天早些到來.
來自:http://www.jianshu.com/p/dab6fd2ed215