Swift Tips:一、Swift新元素

結構體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ù)都必須是同一種類型的。

初始化方法順序

子類初始化順序是:

  1. 給子類自己的所有成員變量初始化
  2. 調用父類相應的初始化方法
  3. 操作父類資源
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方法非常不安全:

  1. 沒有人能保證init只被調用一次
  2. 沒有人能保證在初始化方法調用以后,實例的各個變量都完成了初始化
  3. 如果在初始化里使用屬性進行設置的話,可能會造成各種問題,雖然Apple明確說明不應該在init中使用屬性來訪問,但編譯器沒有做強制,實際還會有很多開發(fā)者犯這種錯誤。

所以Swift有超級嚴格的初始化方法。

  1. designated修飾的初始化方法確保所有非Optional實例變量都被賦值初始化。
  2. 子類會強制(顯式或隱式)調用super的designated初始化方法。

convenience初始化方法是“二等公民”,只作為補充和提供使用上的方便。

  1. convenience方法是不能被子類重寫或者從子為中以super的方式被調用的。
  2. 如果子類想繼承父類的convenience方法,需要重寫這個方法所需要的init方法。

原則:

  1. 初始化路徑必須保證對象完全初始化,這可以通過調用本類型的designated初始化方法來得到保證;
  2. 子類的designated初始化方法必須調用父類的designated方法,以保證父類也完成初始化。

對初始化方法添加required關鍵字,在某些時候非常有必要:

  1. 保證依賴于某個designated初始化方法的convenience一直可以被使用
  2. 給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提供了兩個屬性觀察方法,分別是willSetdidSet
willSet中可以通過newValue獲取即將要設定的值。
didSet中可以通過oldValue獲取舊值。
willSetdidSet是存儲屬性特有的,而getset計算屬性特有的,它們不能同時存在。
子類可以重寫父類的計算屬性為存儲屬性,從而實現(xiàn)willSet或didSet。

lazy方法

Swift標準庫有一組lazy方法,或者說是計算屬性,可以把mapfilter這類接受閉包運行的方法實現(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
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容