結構體mutating
結構體自身是不可變的,因為其整體是一個值類型。如果其內的方法想修改自己的屬性,需要在方法前加關鍵字mutating來讓自身變成另一個值。
protocol的mutating
如果protocol定義了屬性,又定義了方法來改變那個屬性,那么所定義的方法應使用mutating修飾。之所以這么做,是因為struct和enum都可以遵循protocol,它們在實現(xiàn)改變屬性的方法時,要求這個方法是mutating的。
for...in
for...in可以用在所有實現(xiàn)了Sequence協(xié)議的類型上。除了swift原生的集合類型,還可以用在我們自己的類型上。
autoclosure
可以在方法的閉包參數(shù)前使用@autoclosure,這樣在調用方法時,可以只寫閉包體,不需要花括號。對這樣的閉包有一個條件,就是要求它是無參的。
// 定義方法,接受無參閉包
func logIfTrue( _ predicate: @autoclosure () -> Bool) {
if predicate() {
print("True")
}
}
// 調用
logIfTrue(2 > 1)
操作符??的實現(xiàn)也用到了@autoclosure,實際上??后的表達式會轉換成閉包,在需要時才會執(zhí)行所寫表達式。
func的參數(shù)修飾
func接受的參數(shù),如果沒有var修飾,都是let的,默認不可對其重新賦值。
如果你想讓func對參數(shù)的修改影響到外面,可以使用inout修飾這個參數(shù)。
swift是一門討厭變化的語言,所有有可能的地方,都被默認認為是不可變的,也就是用let進行聲明的。這樣不僅可以確保安全,也能在編譯器的性能優(yōu)化上更有作為。
方法嵌套
Swift的方法是一等公民,它可被當作變量或者參數(shù)來使用,甚至可以在方法內定義方法。
有時候我們某個方法過長,會把它拆出幾個子方法來調用,這樣一般會讓被拆出的方法與本來的方法同級,然而這些新增的方法并不應該有這么高的地位,它只需要被原方法調用就足夠,這時候方法嵌套機制就很能發(fā)揮作用了。
命名空間
Swift的命名空間是基于module而不是在代碼中顯式地指明,每個module代表了Swift中的一個命名空間。而一個module對應的就是一個target,也就是說,同一個target里的類型還是不可以同名的。
如果同時用到了分處于不同target的同名類時,可以在類名前加上module名以限定。
MyFramework.MyClass.func()
如果想在同一個target里實現(xiàn)命名空間的效果,還是有折衷的辦法的??梢允褂妙愋颓短祝话阕龇ㄊ前杨愋颓短自趕truct內定義,而外層的struct僅作為命名空間的存在。
struct MyNamespace {
class MyClass {
// ...
}
}
雖然這兩種辦法確實都不是我們所習慣的命名空間,但它們還是能發(fā)揮出命名空間的作用的,至少我們不需要給類名加上各種奇怪的前綴了。
Any和AnyObjct
這兩個都是Swift中的妥協(xié)產(chǎn)物。
AnyObject可以代表任何class類型的實例
Any可以表示任意類型,包括struc,enum,和 func
現(xiàn)在的Swift最主要的用途依然是使用Cocoa框架進行app開發(fā),因此為了與Cocoa架構協(xié)作,將原來id的概念使用了一個類似功能的AnyObject來替代。
AnyObject其實是一個接口:
protocol AnyObject {
}
所有的class都隱式地實現(xiàn)了這個接口。
對變量聲明為AnyObject類型時,有時會發(fā)生Cocoa類型的自動轉換,例如編譯器會把Swift的原生Int類型轉換成Cocoa的NSNumber類型,再存入AnyObject聲明的變量中。
這里好一點的做法是使用Any,它能直接保存Swift原生類型。
我們應盡量使用Swift原生類型,這對性能的提升是有幫助的。
其實使用Any和AnyObject都不是令人愉悅的事情,這都是為妥協(xié)而存在的產(chǎn)物。如果在我們自己代碼里需要大量經(jīng)常地使用這兩者的話,往往意味著代碼可能在結構和設計上存在問題。最好避免依賴和使用這兩者,應嘗試明確指出類型。
typealias和泛型接口
typealias是有用來為已經(jīng)存在的類型重新定義名字的,通過命名,可以使代碼變得更加清晰。
Swift中是沒有泛型接口的,但是可以使用typealias實現(xiàn)泛型的功能。
比如系統(tǒng)的IteratorProtocol接口
protocol IteratorProtocol {
associatedtype Element
public mutating func next() -> Self.Element?
}
然后實現(xiàn)此接口時,會要求實現(xiàn)對應的associatedtype,此時可使用typealias來實現(xiàn)。
class MyIterator: IteratorProtocol {
typealias Element = Int
mutating func next() -> Int? {
return ...
}
}
可變參數(shù)函數(shù)
在Swift中是很容易聲明可變參數(shù)的函數(shù)的,只需要在可變參數(shù)類型后面加上...就可以了。
比如下面這個累加函數(shù):
func sum(input: Int...) -> Int {
return input.reduce(0, +)
}
input在函數(shù)體內部將被作為數(shù)組[Int]來使用。
在其他很多語言中,因為編譯器和語言自身語法特性的限制,可變參數(shù)往往只能作為方法中的最后一個參數(shù)。這個限制在Swift中是不存在的,因為我們會對方法的參數(shù)進行命名,所以可以隨意地放置位置。
然而限制還是有的:
在同一個方法中只能有一個參數(shù)是可變的;
可變參數(shù)都必須是同一種類型的。
初始化方法順序
子類初始化順序是:
- 給子類自己的所有成員變量初始化
- 調用父類相應的初始化方法
- 操作父類資源
class Cat {
var name: String
init() {
name = "cat"
print("cat init")
}
}
class Tiger: Cat {
let power: Int
override init() {
power = 10
// 這里雖然沒有顯式寫super.init()
// 但由于是初始化的最后了,Swift可以替我們自動完成
}
}
上面代碼的Tiger類并沒有第3步,這時可以把第2步代碼省去,Swift自動幫我們完成。
Designated,Convenience 和 Required
Swift的初始化機制相比OC復雜得多,不妨想想Swift這么設計,是要達到一種怎樣的目的。
其實就是安全。在OC中,init方法非常不安全:
- 沒有人能保證
init只被調用一次 - 沒有人能保證在初始化方法調用以后,實例的各個變量都完成了初始化
- 如果在初始化里使用屬性進行設置的話,可能會造成各種問題,雖然Apple明確說明不應該在init中使用屬性來訪問,但編譯器沒有做強制,實際還會有很多開發(fā)者犯這種錯誤。
所以Swift有超級嚴格的初始化方法。
- designated修飾的初始化方法確保所有非Optional實例變量都被賦值初始化。
- 子類會強制(顯式或隱式)調用super的designated初始化方法。
convenience初始化方法是“二等公民”,只作為補充和提供使用上的方便。
-
convenience方法是不能被子類重寫或者從子為中以super的方式被調用的。 - 如果子類想繼承父類的
convenience方法,需要重寫這個方法所需要的init方法。
原則:
- 初始化路徑必須保證對象完全初始化,這可以通過調用本類型的designated初始化方法來得到保證;
- 子類的designated初始化方法必須調用父類的designated方法,以保證父類也完成初始化。
對初始化方法添加required關鍵字,在某些時候非常有必要:
- 保證依賴于某個designated初始化方法的convenience一直可以被使用
- 給convenience方法加上required,以要求子類不要直接使用父類的convenience。
protocol組合
可以把多個protocol加起來合成一個新的協(xié)議,當然新的協(xié)議可以給個名字重新定義出來:
protocol A {
func echo() -> Int
}
protocol B {
func echo() -> String
}
protocol AB: A, B {}
也可以匿名創(chuàng)建,然后用typealias取別名,這種做法優(yōu)于上面的空協(xié)議做法:
typealias AB = A & B
如果同時實現(xiàn)的兩個協(xié)議中有同名方法,在調用時會有點尷尬,編譯器也會直接報錯,這時需要把實例轉成其中一個協(xié)議類型,再調用。
class ABClass: AB {
func echo() -> Int {
return 1
}
func echo() -> String {
return "1"
}
}
// 調用
let ab = ABClass()
let intValue = (ab as A).echo()
let stringValue = (ab as B).echo()
多類型和容器
Swift的原生容器有三種,分別是Array、Dictionary和Set。
它們都是泛型的,也就是說在集合中只能放同一種類型的元素。
容器的泛型除了可以指定實體類型外,還可以指定為協(xié)議protocol。
假如需要在容器中放不同類型的元素時,你應該找到它們的共同點,把共同點抽象為協(xié)議,讓不同類型遵循協(xié)議,然后聲明容器的泛型為這個共同的遵循的協(xié)議。
除了新建一個協(xié)議把不同類型的共同點抽象出來以外,還可以使用枚舉enum,借助swift的枚舉可以帶有值的特點,把對象包起來再存進容器。
enum IntOrString {
case intValue(Int)
case stringValue(String)
}
let mixed = [IntOrString.intValue(1),
IntOrString.stringValue("two"),
IntOrString.intValue(3)]
模式匹配
在Swift中可以用~=符號來進行模式匹配,目前只支持最簡單的相等和范圍匹配。
~=的左邊寫模式,右邊寫被檢驗值。
// 范圍匹配
let isInRange = 3...8 ~= 6
print("isInRange:\(isInRange)")
// 相同匹配
let isEqual = "Swift" ~= "swift"
print("isEqual:\(isEqual)")
// isInRange:true
// isEqual:false
Swift的switch就是使用了~=操作符進行模式匹配
AnyClass,元類型和.self
AnyClass是這么定義的:
typealias AnyClass = Swift.AnyObject.Type
通過AnyObject.Type這種方式所得到的是一個元類型(Meta)。
.Type表示取某個類型的元類型。
.self如果加在類型后面,表示取這個類型本身,如果加在實例后面,表示取這個實例本身。
class A {
// ...
}
let typeA: A.Type = A.self
如果想取protocol的元類型,可以使用.Protocol來獲?。?/p>
protocol P {
// ..
}
let typeP: P.Protocol = P.self
接口和類方法中的Self
首字母大寫的Self表示本類型,一般用于聲明方法。除了指代本類型,還同時指代本類型的子類。
假如要實現(xiàn)一個復制協(xié)議,可以這樣寫:
protocol Copyable {
func copy() -> Self
}
class MyClass {
var num = 1
required init() {
}
func copy() -> Self {
let myType = type(of: self)
let obj = myType.init()
obj.num = num
return obj
}
}
class MySubClass: MyClass {
}
// 調用
let object = MyClass()
object.num = 100
let newObject = object.copy()
object.num = 1
print(object.num)
print(newObject.num)
let subObject = MySubClass()
subObject.num = 200
let newSubObject = subObject.copy()
subObject.num = 2
print(subObject.num)
print(newSubObject.num)
// 打印結果:
1
100
2
200
動態(tài)類型和多方法
Swift不能根據(jù)對象在動態(tài)時的類型進行合適的重載方法調用。
它在編譯階段就已經(jīng)確定了最終的方法調用。
比如:
func printPet(pet: Pet) {
print("Pet")
}
func printPet(pet: Cat) {
print("Cat")
}
func printPet(pet: Dog) {
print("Pet")
}
func printThem(pet: Pet, cat: Cat) {
printPet(pet: pet)
printPet(pet: cat)
}
// 調用
printThem(pet: Dog(), cat: Cat())
// 打印結果:
Pet
Cat
printThem方法的pet參數(shù)聲明了接收一個Pet對象,那么其內的printPet方法就實實在在地調用了入?yún)镻et的方法,而不管實際運行時是否是Pet的子類。
屬性觀察
Swift提供了兩個屬性觀察方法,分別是willSet和didSet。
在willSet中可以通過newValue獲取即將要設定的值。
在didSet中可以通過oldValue獲取舊值。
willSet和didSet是存儲屬性特有的,而get和set計算屬性特有的,它們不能同時存在。
子類可以重寫父類的計算屬性為存儲屬性,從而實現(xiàn)willSet或didSet。
lazy方法
Swift標準庫有一組lazy方法,或者說是計算屬性,可以把map和filter這類接受閉包運行的方法實現(xiàn)延時運行。對于運行代價很大的情況,它可以起到不小的性能提升作用。
本來不使用lazy是這樣的:
let data = 1...3
let result = data.map { (i) -> Int in
print("正在處理\(i)")
return i * 2
}
print("準備訪問結果")
for i in result {
print("操作后的結果為\(i)")
}
print("操作完畢")
打印結果為:
正在處理1
正在處理2
正在處理3
準備訪問結果
操作后的結果為2
操作后的結果為4
操作后的結果為6
操作完畢
在沒有l(wèi)azy時,map會按順序直接運行閉包。
下面先取其lazy結果,后再map:
let data = 1...3
let result = data.lazy.map { (i) -> Int in
print("正在處理\(i)")
return i * 2
}
print("準備訪問結果")
for i in result.reversed() {
print("操作后的結果為\(i)")
}
print("操作完畢")
打印結果為:
準備訪問結果
正在處理3
操作后的結果為6
正在處理2
操作后的結果為4
正在處理1
操作后的結果為2
操作完畢
lazy后,可以在實際訪問序列元素時,才執(zhí)行閉包運算。
Reflection和Mirror
現(xiàn)在的Swift雖然在反射方面相比Objective-C要弱得多,但還是存在一些和反射相關的內容的。
可以通過一個Mirror來獲取某元素的一些信息,比如對象的所有屬性。
struct Person {
let name: String
let age: Int
}
let xiaoMing = Person(name: "XiaoMing", age: 16)
let r = Mirror(reflecting: xiaoMing)
print("xiamMing是\(r.displayStyle!)")
print("屬性個數(shù):\(r.children.count)")
for child in r.children {
print("屬性名:\(child.label!), 值:\(child.value)")
}
打印結果:
xiamMing是struct
屬性個數(shù):2
屬性名:name, 值:XiaoMing
屬性名:age, 值:16
也可以用dump打印其鏡像信息:
dump(xiaoMing)
? Person
- name: "XiaoMing"
- age: 16
Optional Map
Optional實際上是一個枚舉,?只是它的語法糖。
enum Optional<Wrapped> : ExpressibleByNilLiteral {
case none
case some(Wrapped)
}
現(xiàn)在假設我們有個需求,要將某個Int?乘2。一個合理的策略是如果這個Int?有值的話,就取出值進行乘2操作,如果是nil的話就直接將nil賦給結果。我們可以寫出如下代碼:
let num: Int? = 3
var result: Int?
if let realNum = num {
result = realNum * 2
} else {
result = nil
}
其實我們有更優(yōu)雅簡潔的方式,那就是使用Optional的map。它內部定義了map方法,當然也還有flatMap:
public func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U?
public func flatMap<U>(_ transform: (Wrapped) throws -> U?) rethrows -> U?
這樣我們可以很方便地對一個Optional值做變化和操作,而不必手動解包。
let num: Int? = 3
let result = num.map {
$0 * 2
}
Protocol Extension
對Protocol進行Extension擴展,可以給Protocol聲明的方法做默認實現(xiàn)。
但是,在protocol的extension中實現(xiàn)了接口里沒有定義的方法時,就比較有趣了。
extension PA {
func method1() {
print("protocol method1")
}
func method2() {
print("protocol method2")
}
}
struct SA: PA {
func method1() {
print("struct method1")
}
func method2() {
print("struct method2")
}
}
// 調用
let s = SA()
s.method1()
s.method2()
print("轉換為Protocol后:")
let p = s as PA
p.method1()
p.method2()
打印結果:
struct method1
struct method2
轉換為Protocol后:
struct method1
protocol method2
SA實現(xiàn)了PA協(xié)議,當變量作為struct使用時,編譯器選擇了調用struct的方法。
當變量作為protocol使用時,對于method1,編譯器知道其協(xié)議實現(xiàn)者必定實現(xiàn)了method1方法,故使用動態(tài)派發(fā),在運行時調用實現(xiàn)類型的方法;對于method2,protocol并沒有聲明這個方法,編譯器不確定協(xié)議實現(xiàn)者有沒有實現(xiàn)這個方法,所以它直接確定了調用extension的method2,而不管協(xié)議實現(xiàn)者。
where和模式匹配
where關鍵字可用在switch的case語句中,對條件進行限定:
let name = ["王小二","張三","李四","王二小"]
name.forEach {
switch $0 {
case let x where x.hasPrefix("王"):
print("\(x)是筆者本家")
default:
print("你好,\($0)")
}
}
但如果想在if語句中對條件進行限定,現(xiàn)在的swift版本要用逗號,代替where:
let num: [Int?] = [48, 61, nil]
num.forEach {
if let score = $0, score >= 60 {
print("及格啦 - \(score)")
} else {
print("不及格 :(")
}
}
where還廣泛用在Protocol中,比如限定associatedtype:
public protocol Sequence {
associatedtype Element where Self.Element == Self.Iterator.Element
}
以及限定擴展的適用條件:
extension Sequence where Self.Element : Comparable {
// ...
}
參考
- Swifter - 100個 Swift 必備 Tips