Swift開發小記(含面試題)

春節先後一直在忙晉升的事情,整理了下以前記錄的Swift知識點,同時在網上看到的一些有意思的面試題我也加了進來,有的題目我稍微變了下,感受會更有趣,固然老規矩全部代碼在整理時都從新跑了一遍確認~ 另外後續文章的更新頻率加快,除了iOS開發上的一些文章,也會增長一些跨平臺、前端、算法等文章,也算是對本身技術棧的回顧吧

聲明


  • let和var前端

    let用來聲明常量,var用來聲明變量。瞭解js的對於這兩個應該不陌生,可是區別仍是挺大的,尤爲是let,在js中是用來聲明變量的,const纔是用來聲明常量的。git

  • 類型標註面試

    聲明常量/變量時能夠增長類型標註,來講明存儲類型,例如算法

    var message: String
    複製代碼

    若是不顯示說明,Swift會根據聲明時賦值自動推斷出對應類型。通常不太須要標註類型,可是以前遇到過在某些狀況下須要大量聲明時,因爲沒有標註類型,Xcode直接內存爆了致使Mac死機,後來加上就行了。編程

  • 命名swift

    常量與變量名不能包含數學符號,箭頭,保留的(或者非法的)Unicode 碼位,連線與製表符。也不能以數字開頭,可是能夠在常量與變量名的其餘地方包含數字,除此外你能夠用任何你喜歡的字符,例如api

    let 🐶 = ""
    let 蝸牛君 = ""
    複製代碼

    可是不推薦使用,騷操做太多容易閃着腰,Camel-Case還是更好的選擇。數組

    另外若是須要使用Swift保留關鍵字,可使用**反引號`**包裹使用,例如安全

    enum BlogStyle {
        case `default`
        case colors
    }
    複製代碼

元組(Tuple)


Swift支持把多個值組合成一個複合值,稱爲元組。元組內的值能夠是任意類型,並不要求是相同類型,例如性能優化

let size = (width: 10, height: 10)
print("\(size.0)")
print("\(size.width)")

// 也能夠不對元素命名
let size = (10, 10)
複製代碼

在函數須要返回多個值時,元組很是有用。但它並不適合複雜的數據表達,而且儘可能只在臨時須要時使用

以前有同事經過元組返回多個值,且沒有對元素命名,而後很多地方都使用了該元組,致使後面的同事接手時沒法快速理解數據含義,而且在須要改動返回數據時,必須經過代碼邏輯去查找哪些地方使用了該元組,耗費了很多時間。
複製代碼

可選類型


可選類型是Swift區別與ObjC的另外一個新特性,它用於處理值可能缺失的狀況,能夠經過在類型標註後面加上一個?來表示這是一個可選類型,例如

// 可選類型沒有明確賦值時,默認爲nil
var message: String?
// 要麼有確切值
message = "Hello"
// 要麼沒有值,爲nil
message = nil
複製代碼

Optional其實是一個泛型枚舉,大體源碼以下:

public enum Optional<Wrapped> : ExpressibleByNilLiteral {

    /// The absence of a value.
    ///
    /// In code, the absence of a value is typically written using the `nil`
    /// literal rather than the explicit `.none` enumeration case.
    case none

    /// The presence of a value, stored as `Wrapped`.
    case some(Wrapped)
  
  	....
}
複製代碼

因此上面的初始化還能夠這麼寫:

str = .none
複製代碼
Swift 的 nil 和 ObjC 中的 nil 並不同。在 ObjC 中,nil 是一個指向不存在對象的指針。在 Swift 中,nil 不是指針——它是一個肯定的值,用來表示值缺失。任何類型的可選狀態均可以被設置爲 nil,不僅是對象類型。
複製代碼
  • 可選綁定

    可是在平常開發過程當中,經常咱們須要判斷可選類型是否有值,而且獲取該值,這時候咱們就要用到可選綁定

    if let message = message {
      ...
    }
    
    guard let message = message else {
      return
    }
    ...
    複製代碼

    上述代碼能夠理解爲:若是 message 返回的可選 String 包含一個值,建立一個叫作 message 的新常量並將可選包含的值賦給它。

  • 隱式解析可選類型

    雖然咱們能夠經過可選綁定來獲取可選類型的值,可是對於一些除了初始化時爲nil,後續再也不爲nil的值來講,使用可選綁定會顯的臃腫.

    隱式解析可選類型支持在可選類型後面加!來直接獲取有效值, 例如

    let message: String = message!
    複製代碼

    若是你在隱式解析可選類型沒有值的時候嘗試取值,會觸發運行時錯誤。所以Apple建議若是一個變量以後可能變成nil的話請不要使用隱式解析可選類型,而是使用普通可選類型。

    但事實是靠人爲去判別是否能使用隱式解析可選類型是很是危險的,尤爲是團隊合做,一旦出問題就會形成崩潰,所以在咱們團隊不容許使用隱式解析可選類型。
    複製代碼

運算符


  • 空合運算符

    空合運算符a ?? b)將對可選類型 a 進行空判斷,若是 a 包含一個值就進行解封,不然就返回一個默認值 b。表達式 a 必須是 Optional 類型。默認值 b 的類型必需要和 a 存儲值的類型保持一致。

    let message: String = message ?? "Hello"
    // 實際上等同於三目運算符
    message != nil ? message! : "Hello"
    複製代碼
  • 區間運算符

    Swift提供了幾種方便表達一個區間的值的區間運算符,I like it😀

    let array = [1, 2, 3, 4, 5]
    // 閉區間運算符,表示截取下標0~2的數組元素
    array[0...2]
    // 半開區間運算符,表示截取下標0~1的數組元素
    array[0..<2]
    // 單側區間運算符,表示截取開始到下標2的數組元素
    array[...2]
    // 單側區間運算符,表示截取從下標2到結束的數組元素
    array[2...]
    複製代碼

    除此以外,還能夠經過 ... 或者 ..< 來鏈接兩個字符串。一個常見的使用場景就是檢查某個字符是不是合法的字符。

    // 判斷是否包含大寫字母,並打印
    let str = "Hello"
    let test = "A"..."Z"
    for c in str {
        if test.contains(String(c)) {
            print("\(c)是大寫字母")
        }
    }
    // 打印 H是大寫字母
    複製代碼
  • 恆等運算符

    有時候咱們須要斷定兩個常量或者變量是否引用同一個類實例。爲了達到這個目的,Swift 內建了兩個恆等運算符:

    • 等價於(===
    • 不等價於(!==

    運用這兩個運算符檢測兩個常量或者變量是否引用同一個實例。

  • ++和--

    Swift不支持這種寫法,ObjC還用得蠻多的。

閉包


閉包是自包含的函數代碼塊,能夠在代碼中被傳遞和使用。Swift 中的閉包與 C 和 ObjC 中的代碼塊(blocks)比較類似。

Swift 的閉包表達式擁有簡潔的風格,並鼓勵在常見場景中進行語法優化,主要優化以下:

-利用上下文推斷參數和返回值類型

-隱式返回單表達式閉包,即單表達式閉包能夠省略 return 關鍵字

-參數名稱縮寫

-尾隨閉包語法

閉包表達式語法有以下的通常形式:

{ (parameters) -> returnType in
    statements
}
複製代碼
  • 尾隨閉包

    當函數的最後一個參數是閉包時,可使用尾隨閉包來加強函數的可讀性。在使用尾隨閉包時,你不用寫出它的參數標籤:

    func test(closure: () -> Void) {
        ...
    }
    
    // 不使用尾隨閉包
    test(closure: {
        ...
    })
    
    // 使用尾隨閉包
    test() {
        ...
    }
    複製代碼
  • 逃逸閉包

    當一個閉包做爲參數傳到一個函數中,可是這個閉包在函數返回以後還可能被使用,咱們稱該閉包從函數中逃逸。例如

    var completions: [() -> Void] = []
    func testClosure(completion: () -> Void) {
        completions.append(completion)
    }
    複製代碼

    此時編譯器會報錯,提示你這是一個逃逸閉包,咱們能夠在參數名以前標註 @escaping,用來指明這個閉包是容許「逃逸」出這個函數的。

    var completions: [() -> Void] = []
    func testEscapingClosure(completion: @escaping () -> Void) {
        completions.append(completion)
    }
    複製代碼

    另外,將一個閉包標記爲 @escaping 意味着你必須在閉包中顯式地引用 self,而非逃逸閉包則不用。這提醒你可能會一不當心就捕獲了self,注意循環引用。

  • 自動閉包

    自動閉包是一種自動建立的閉包,用於包裝傳遞給函數做爲參數的表達式。這種閉包不接受任何參數,讓你可以省略閉包的花括號,用一個普通的表達式來代替顯式的閉包。

    而且自動閉包讓你可以延遲求值,由於直到你調用這個閉包,代碼段纔會被執行。要標註一個閉包是自動閉包,須要使用@autoclosure

    // 未使用自動閉包,須要顯示用花括號說明這個參數是一個閉包
    func test(closure: () -> Bool) {
    }
    test(closure: { 1 < 2 } )
    
    // 使用自動閉包,只須要傳遞表達式
    func test(closure: @autoclosure () -> String) {
    }
    test(customer: 1 < 2)
    複製代碼

遞歸枚舉


可能有這樣一個場景,定義一個Food的枚舉,包含了一些食物,同時還支持基於這些食物能夠兩兩混合成新食物

enum Food {
    case beef
    case potato
    case mix(Food, Food)
}
複製代碼

此時編譯器會提示你Recursive enum 'Food' is not marked 'indirect',緣由是由於枚舉成員裏出現了遞歸調用。所以咱們須要在枚舉成員前加上indirect來表示該成員可遞歸。

// 標記整個枚舉是遞歸枚舉
indirect enum Food {
    case beef
    case potato
    case mix(Food, Food)
}

// 僅標記存在遞歸的枚舉成員
enum Food {
    case beef
    case potato
    indirect case mix(Food, Food)
}
複製代碼

更推薦第二種寫法,由於使用遞歸枚舉時,編譯器會插入一個間接層。僅標記枚舉成員,可以減小沒必要要的消耗。

屬性


  • 存儲屬性

    簡單來講,一個存儲屬性就是存儲在特定類或結構體實例裏的一個常量或變量。存儲屬性能夠是變量存儲屬性(用關鍵字 var 定義),也能夠是常量存儲屬性(用關鍵字 let 定義)。

    struct Person {
        var name: String
        var height: CGFloat
    }
    複製代碼

    同時咱們還能夠經過Lazy來標示該屬性爲延遲存儲屬性,相似於ObjC常說的的懶加載。

    lazy var fileName: String = "data.txt"
    複製代碼
    若是一個被標記爲 lazy 的屬性在沒有初始化時就同時被多個線程訪問,則沒法保證該屬性只會被初始化一次,也就是說它是非線程安全的。
    複製代碼
  • 計算屬性

    除存儲屬性外,類、結構體和枚舉能夠定義計算屬性。計算屬性不直接存儲值,而是提供一個 getter 和一個可選的 setter,來間接獲取和設置其餘屬性或變量的值。

    struct Rect {
        var origin = CGPoint.zero
        var size = CGSize.zero
        var center: CGPoint {
            get {
                let centerX = origin.x + (size.width / 2)
                let centerY = origin.y + (size.height / 2)
                return Point(x: centerX, y: centerY)
            }
            set {
                origin.x = newValue.x - (size.width / 2)
                origin.y = newValue.y - (size.height / 2)
            }
        }
    }
    複製代碼

    若是咱們只但願可讀而不可寫時,setter方法不提供便可,能夠簡寫爲

    var center: CGPoint {
        let centerX = origin.x + (size.width / 2)
        let centerY = origin.y + (size.height / 2)
        return Point(x: centerX, y: centerY)
    }
    複製代碼
  • 觀察器

    Swift提供了很是方便的觀察屬性變化的方法,每次屬性被設置值的時候都會調用屬性觀察器,即便新值和當前值相同的時候也不例外。

    var origin: CGPoint {
        willSet {
            print("\(newValue)")
        }
        didSet {
            print("\(oldValue)")
        }
    }
    複製代碼
  • 調用時序

    調用 numberset 方法能夠看到工做的順序

    let b = B()
    b.number = 0
    
    // 輸出
    // get
    // willSet
    // set
    // didSet
    複製代碼

    爲何有個get

    這是由於咱們實現了 didSetdidSet 中會用到 oldValue,而這個值須要在整個 set 動做以前進行獲取並存儲待用,不然將沒法確保正確性。若是咱們不實現 didSet 的話,此次 get 操做也將不存在。

unowned


你能夠在聲明屬性或者變量時,在前面加上關鍵unowned`表示這是一個無主引用。使用無主引用,你必須確保引用始終指向一個未銷燬的實例。

和weak相似,unowned不會緊緊保持住引用的實例。它也被用於解決可能存在循環引用,且對象是非可選類型的場景。

例如在這樣的設計中:一個客戶可能有或者沒有信用卡,可是一張信用卡老是關聯着一個客戶。

class Customer {
    let name: String
    var card: CreditCard?
}

class CreditCard {
    let number: UInt64
    unowned let customer: Customer // 因爲始終有值,沒法使用weak
}
複製代碼

is和as


  • is

is 在功能上至關於ObjC的 isKindOfClass,能夠檢查一個對象是否屬於某類型或其子類型。is 和原來的區別主要在於亮點,首先它不只能夠用於 class 類型上,也能夠對 Swift 的其餘像是 structenum 類型進行判斷。

class ClassA { }
class ClassB: ClassA { }

let obj: AnyObject = ClassB()

if (obj is ClassA) {
    print("屬於 ClassA")
}

if (obj is ClassB) {
    print("屬於 ClassB")
}
複製代碼
  • as

某類型的一個常量或變量可能在幕後實際上屬於一個子類。當肯定是這種狀況時,你能夠嘗試向下轉到它的子類型,用類型轉換操做符as?as!)。

class Media {}
class Movie: Media {}
class Song: Media {}

for item in medias {
    if let movie = item as? Movie { 
        print("It's Movie")
    } else if let song = item as? Song {
        print("It's Song")
    }
}
複製代碼

as? 返回一個你試圖向下轉成的類型的可選值。 as! 把試圖向下轉型和強制解包轉換結果結合爲一個操做。只有你能夠肯定向下轉型必定會成功時,才使用as!

如同隱式解析可選類型同樣,as!一樣具備崩潰的高風險,咱們通常不容許使用。
複製代碼

擴展下標


Swift支持經過Extension爲已有類型添加新下標,例如

extension Int {
    subscript(digitIndex: Int) -> Int {
        var decimalBase = 1
        for _ in 0..<digitIndex {
            decimalBase *= 10
        }
        return (self / decimalBase) % 10
    }
}
746381295[0]
// 返回 5
746381295[1]
// 返回 9
746381295[2]
// 返回 2
746381295[8]
// 返回 7
複製代碼

若是該 Int 值沒有足夠的位數,即下標越界,那麼上述下標實現會返回 0,猶如在數字左邊自動補 0

746381295[9]
// 返回 0,即等同於:
0746381295[9]
複製代碼

mutating


結構體和枚舉類型中修改 self 或其屬性的方法必須將該實例方法標註爲 mutating,不然沒法在方法裏改變本身的變量。

struct MyCar {
    var color = UIColor.blue
    mutating func changeColor() {
        color = UIColor.red
    }
}
複製代碼

因爲Swift 的 protocol 不只能夠被 class 類型實現,也適用於 structenum,所以咱們在寫給別人用的接口時須要多考慮是否使用 mutating 來修飾方法。

實現協議中的 mutating 方法時,如果類類型,則不用寫 mutating 關鍵字。而對於結構體和枚舉,則必須寫 mutating 關鍵字。
複製代碼

協議合成


有時候須要同時遵循多個協議,例如一個函數但願某個參數同時知足ProtocolA和ProtocolB,咱們能夠採用 ProtocolA & ProtocolB 這樣的格式進行組合,稱爲 協議合成(protocol composition)

func testComposition(protocols: ProtocolA & ProtocolB) {
}
複製代碼

selector和@objc

在開發中經常有面臨這樣的代碼

btn.addTarget(self, action: #selector(onClick(_:)), for: .touchUpInside)

@objc func onClick(_ sender: UIButton) {
}
複製代碼

爲何要使用@objc?

由於Swift 中的 #selector 是從暴露給 ObjC 的代碼中獲取一個 selector,因此它仍然是 ObjC runtime 的概念,若是你的 selector 對應的方法只在 Swift 中可見的話 (也就是說它是一個 Swift 中的 private 方法),在調用這個 selector 時你會遇到一個 unrecognized selector 錯誤。

inout


有些時候咱們會但願在方法內部直接修改輸入的值,這時候咱們可使用 inout 來對參數進行修飾:

func addOne(_ variable: inout Int) {
    variable += 1
}
複製代碼

由於在函數內部就更改了值,因此也不須要返回了。調用也要改變爲相應的形式,在前面加上 & 符號:

incrementor(&luckyNumber)
複製代碼

單例


在 ObjC 中單例通常寫成:

@implementation MyManager
+ (id)sharedManager {
    static MyManager *staticInstance = nil;
    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{
        staticInstance = [[self alloc] init];
    });
    return staticInstance;
}
@end
複製代碼

但在Swift中變得很是簡潔:

static let sharedInstance = MyManager()
複製代碼

隨機數


咱們經常可能使用這樣的方式來獲取隨機數,好比100之內的:

let randomNum: Int = arc4random() % 100
複製代碼

此時編譯器會提示error,由於arc4random()返回UInt32,須要作類型轉換,有時候咱們可能就直接:

let randomNum: Int = Int(arc4random()) % 100
複製代碼

結果測試的時候發如今有些機型上就崩潰了。

這是由於Int在32位機器上(iPhone5及如下)至關於Int32,64位機器上至關於Int64,表現上與ObjC中的NSInteger一致,而arc4random()始終返回UInt32,因此在32位機器上就可能越界崩潰了。

最快捷的方式能夠先取餘以後再類型轉換:

let randomNum: Int = Int(arc4random() % 100)
複製代碼

可變參數函數


若是想要一個可變參數的函數只須要在聲明參數時在類型後面加上 ... 就能夠了。

func sum(input: Int...) -> Int {
    return input.reduce(0, combine: +)
}

print(sum(1,2,3,4,5))
複製代碼

可選協議


Swift protocol自己不容許可選項,要求全部方法都是必須得實現的。可是因爲Swift和ObjC能夠混編,那麼爲了方便和ObjC打交道,Swift支持在 protocol中使用 optional 關鍵字做爲前綴來定義可選要求,且協議和可選要求都必須帶上@objc屬性。

@objc protocol CounterDataSource {
    @objc optional func incrementForCount(count: Int) -> Int
    @objc optional var fixedIncrement: Int { get }
}
複製代碼

可是標記 @objc 特性的協議只能被繼承自 ObjC 類的類或者 @objc 類遵循,其餘類以及結構體和枚舉均不能遵循這種協議。這對於Swift protocol是一個很大的限制。

因爲 protocol支持可擴展,那麼咱們能夠在聲明一個 protocol以後再用extension的方式給出部分方法默認的實現,這樣這些方法在實際的類中就是可選實現的了。

protocol CounterDataSource {
    func incrementForCount(count: Int) -> Int
    var fixedIncrement: Int { get }
}

extension CounterDataSource {
    func incrementForCount(count: Int) -> Int {
        if count == 0 {
            return 0
        } else if count < 0 {
            return 1
        } else {
            return -1
        }
    }
}

class Counter: CounterDataSource {
    var fixedIncrement: Int = 0
}
複製代碼

協議擴展


有這麼一個例子:

protocol A2 {
    func method1()
}

extension A2 {
    func method1() {
        return print("hi")
    }

    func method2() {
        return print("hi")
    }
}

struct B2: A2 {
    func method1() {
        return print("hello")
    }

    func method2() {
        return print("hello")
    }
}

let b2 = B2()
b2.method1()
b2.method2()
複製代碼

打印的結果以下:

hello
hello
複製代碼

結果看起來在乎料之中,那若是咱們稍做改動:

let a2 = b2 as A2
a2.method1() 
a2.method2() 
複製代碼

此時結果是什麼呢?還與以前同樣麼?打印結果以下:

hello
hi
複製代碼

對於 method1,由於它在 protocol 中被定義了,所以對於一個被聲明爲遵照接口的類型的實例 (也就是對於 a2) 來講,能夠肯定實例必然實現了 method1,咱們能夠放心大膽地用動態派發的方式使用最終的實現 (不論它是在類型中的具體實現,仍是在接口擴展中的默認實現);可是對於 method2 來講,咱們只是在接口擴展中進行了定義,沒有任何規定說它必須在最終的類型中被實現。在使用時,由於 a2 只是一個符合 A2 接口的實例,編譯器對 method2 惟一能肯定的只是在接口擴展中有一個默認實現,所以在調用時,沒法肯定安全,也就不會去進行動態派發,而是轉而編譯期間就肯定的默認實現。

值類型和引用類型


Swift 的類型分爲值類型和引用類型兩種,值類型在傳遞和賦值時將進行復制,而引用類型則只會使用引用對象的一個 "指向"。

  • 值類型有哪些?

    Swift 中的 structenum 定義的類型是值類型,使用 class 定義的爲引用類型。頗有意思的是,Swift 中的全部的內建類型都是值類型,不只包括了傳統意義像 IntBool這些,甚至連 StringArray 以及 Dictionary 都是值類型的。

  • 值類型的好處?

    相較於傳統的引用類型來講,一個很顯而易見的優點就是減小了堆上內存分配和回收的次數。值類型的一個特色是在傳遞和賦值時進行復制,每次複製確定會產生額外開銷,可是在 Swift 中這個消耗被控制在了最小範圍內,在沒有必要複製的時候,值類型的複製都是不會發生的。

    var a = [1,2,3]
    var b = a
    let c = b
    b.append(5) // 此時 a,c 和 b 的內存地址再也不相同
    複製代碼

    只有當值類型的內容發生改變時,值類型被纔會複製。

  • 值類型的弊端?

    在少數狀況下,咱們顯然也可能會在數組或者字典中存儲很是多的東西,而且還要對其中的內容進行添加或者刪除。在這時,Swift 內建的值類型的容器類型在每次操做時都須要複製一遍,即便是存儲的都是引用類型,在複製時咱們仍是須要存儲大量的引用,這個開銷就變得不容忽視了。

  • 最佳實踐

    針對上述問題,咱們能夠經過 Cocoa 中的引用類型的容器類來對應這種狀況,那就是 NSMutableArrayNSMutableDictionary

    因此,在使用數組合字典時的最佳實踐應該是,按照具體的數據規模和操做特色來決定到時是使用值類型的容器仍是引用類型的容器:在須要處理大量數據而且頻繁操做 (增減) 其中元素時,選擇 NSMutableArrayNSMutableDictionary 會更好,而對於容器內條目小而容器自己數目多的狀況,應該使用 Swift 語言內建的 ArrayDictionary

獲取對象類型


let str = "Hello"
print("\(type(of: str))")
print("\(String(describing: object_getClass(str)))")

// String
// Optional(NSTaggedPointerString)
複製代碼

KVO


KVO在Cocoa中是很是強大的特性,在ObjC中有很是多的應用,以前在《iOS開發小記-基礎篇》中有相關介紹,感興趣的同窗能夠移步這篇文章。

在 Swift 中咱們也是可使用 KVO 的,可是僅限於在 NSObject 的子類中。這是能夠理解的,由於 KVO 是基於 KVC (Key-Value Coding) 以及動態派發技術實現的,而這些東西都是 ObjC 運行時的概念。另外因爲 Swift 爲了效率,默認禁用了動態派發,所以想用 Swift 來實現 KVO,咱們還須要作額外的工做,那就是將想要觀測的對象標記爲 @objc dynamic`。

例如,按照ObjC的使用習慣,咱們每每會這麼實現:

class Person: NSObject {
    @objc dynamic var isHealth = true
}

private var familyContext = 0
class Family: NSObject {

    var grandpa: Person

    override init() {
        grandpa = Person()
        super.init()
        print("爺爺身體情況: \(grandpa.isHealth ? "健康" : "不健康")")
        grandpa.addObserver(self, forKeyPath: "isHealth", options: [.new], context: &familyContext)
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
            self.grandpa.isHealth = false
        }
    }

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if let isHealth = change?[.newKey] as? Bool,
            context == &familyContext {
            print("爺爺身體情況發生了變化:\(isHealth ? "健康" : "不健康")")
        }
    }
}
複製代碼

但實際上Swift 4經過閉包優化了KVO的實現,咱們能夠將上述例子改成:

class Person: NSObject {
    @objc dynamic var isHealth = true
}

class Family: NSObject {

    var grandpa: Person
    var observation: NSKeyValueObservation?

    override init() {
        grandpa = Person()
        super.init()
        print("爺爺身體情況: \(grandpa.isHealth ? "健康" : "不健康")")

        observation = grandpa.observe(\.isHealth, options: .new) { (object, change) in
            if let isHealth = change.newValue {
                print("爺爺身體情況發生了變化:\(isHealth ? "健康" : "不健康")")
            }
        }

        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
            self.grandpa.isHealth = false
        }
    }
}
複製代碼

這樣看上去是否是友好了許多?

《WWDC-What's New in Foundation》專題上介紹 KVO 時,提到了observe會返回一個 NSKeyValueObservation對象,開發者只須要管理它的生命週期,而再也不須要移除觀察者,所以不用擔憂忘記移除而致使crash。

你能夠試試將observation = grandpa.observe改成let observation = grandpa.observe,看看有什麼不一樣?

  • 開發上的弊端

    在 ObjC 中咱們幾乎能夠沒有限制地對全部知足 KVC 的屬性進行監聽,而如今咱們須要屬性有 @objc dynamic 進行修飾。但在不少狀況下,監聽的類屬性並不知足這個條件且沒法修改。目前可行的一個方案是經過屬性觀察器來實現一套本身的相似替代。

    喵神在關於這一節的講述,因爲Swift版本較早,提出「一個可能可行的方案是繼承這個類而且將須要觀察的屬性使用 `dynamic` 進行重寫。」
    但實際上僅使用`dynamic`修飾是不夠的,Swift 4開始還得配合`@objc`使用,可是繼承後再添加`@objc`是沒法編譯經過的。(這一點也容易理解,由於父類對於ObjC來講,已經不是`NSObject`的子類了)
    複製代碼

lazy


前面提到過lazy能夠用來標示屬性延遲加載,它還能夠配合像 map 或是 filter 這類接受閉包並進行運行的方法一塊兒,讓整個行爲變成延時進行的。在某些狀況下這麼作也對性能會有不小的幫助。

例如,直接使用 map 時:

let data = 1...3
let result = data.map {
    (i: Int) -> Int in
    print("正在處理 \(i)")
    return i * 2
}

print("準備訪問結果")
for i in result {
    print("操做後結果爲 \(i)")
}

print("操做完畢")
複製代碼

其輸出爲:

// 正在處理 1
// 正在處理 2
// 正在處理 3
// 準備訪問結果
// 操做後結果爲 2
// 操做後結果爲 4
// 操做後結果爲 6
// 操做完畢
複製代碼

而若是咱們先進行一次 lazy 操做的話,咱們就能獲得延時運行版本的容器:

let data = 1...3
let result = data.lazy.map {
    (i: Int) -> Int in
    print("正在處理 \(i)")
    return i * 2
}

print("準備訪問結果")
for i in result {
    print("操做後結果爲 \(i)")
}

print("操做完畢")
複製代碼

此時的運行結果:

// 準備訪問結果
// 正在處理 1
// 操做後結果爲 2
// 正在處理 2
// 操做後結果爲 4
// 正在處理 3
// 操做後結果爲 6
// 操做完畢
複製代碼

對於那些不須要徹底運行,可能提早退出的狀況,使用 lazy 來進行性能優化效果會很是有效。

Log與編譯符號


有時候咱們會想要將當前的文件名字和那些必要的信息做爲參數一塊兒打印出來,Swift 爲咱們準備了幾個頗有用的編譯符號,用來處理相似這樣的需求,它們分別是:

符號 類型 描述
#file String 包含這個符號的文件的路徑
#line Int 符號出現處的行號
#column Int 符號出現處的列
#function String 包含這個符號的方法名字

這樣,咱們能夠經過使用這些符號來寫一個好一些的 Log 輸出方法:

override func viewDidLoad() {
    super.viewDidLoad()

    detailLog(message: "嘿,這裏有問題")
}

func detailLog<T>(message: T, file: String = #file, method: String = #function, line: Int = #line) {
    #if DEBUG
    print("\((file as NSString).lastPathComponent)[\(line)], \(method): \(message)")
    #endif
}
複製代碼

Optional Map


咱們經常會對數組使用 map 方法,這個方法能對數組中的全部元素應用某個規則,而後返回一個新的數組。

例如但願將數組中的全部數字乘2:

let nums = [1, 2, 3]
let result = nums.map{ $0 * 2 }
print("\(result)")

// 輸出:[2, 4, 6]
複製代碼

但若是改爲對某個Int?乘2呢?指望若是這個 Int? 有值的話,就取出值進行乘 2 的操做;若是是 nil 的話就直接將 nil 賦給結果。

let num: Int? = 3
// let num: Int? = nil

var result: Int?
if let num = num {
    result = num * 2
}
print("\(String(describing: result))")

// num = 3時,打印Optional(6)
// num = nil時,打印nil
複製代碼

但其實有更優雅簡潔的寫法,那就是Optional Map。不只在 Array 或者說 CollectionType 裏能夠用 map,在 Optional 的聲明的話,會發現它也有一個 map 方法:

/// Evaluates the given closure when this `Optional` instance is not `nil`,
/// passing the unwrapped value as a parameter.
///
/// Use the `map` method with a closure that returns a non-optional value.
/// This example performs an arithmetic operation on an
/// optional integer.
///
/// let possibleNumber: Int? = Int("42")
/// let possibleSquare = possibleNumber.map { $0 * $0 }
/// print(possibleSquare)
/// // Prints "Optional(1764)"
///
/// let noNumber: Int? = nil
/// let noSquare = noNumber.map { $0 * $0 }
/// print(noSquare)
/// // Prints "nil"
///
/// - Parameter transform: A closure that takes the unwrapped value
/// of the instance.
/// - Returns: The result of the given closure. If this instance is `nil`,
/// returns `nil`.
@inlinable public func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U?
複製代碼

如同函數說明描述,這個方法能讓咱們很方便地對一個 Optional 值作變化和操做,而沒必要進行手動的解包工做。輸入會被自動用相似 Optinal Binding 的方式進行判斷,若是有值,則進入 transform的閉包進行變換,並返回一個 U?;若是輸入就是 nil 的話,則直接返回值爲 nilU?

所以,剛纔的例子能夠改成:

let num: Int? = 3
// let num: Int? = nil

let result = num.map { $0 * 2 }
print("\(String(describing: result))")

// num = 3時,打印Optional(6)
// num = nil時,打印nil
複製代碼

Delegate


在一開始在Swift寫代理時,咱們可能會這麼寫:

protocol MyProyocol {
    func method()
}

class MyClass: NSObject {
    weak var delegate: MyProyocol?
}
複製代碼

而後就會發現編譯器會提示錯誤'weak' must not be applied to non-class-bound 'MyProyocol'; consider adding a protocol conformance that has a class bound

這是由於 Swift 的 protocol 是能夠被除了 class 之外的其餘類型遵照的,而對於像 struct 或是 enum 這樣的類型,自己就不經過引用計數來管理內存,因此也不可能用 weak 這樣的 ARC 的概念來進行修飾。

所以想要在 Swift 中使用 weak delegate,咱們就須要將 protocol 限制在 class 內,例如

protocol MyProyocol: class {
    func method()
}

protocol MyProyocol: NSObjectProtocol {
    func method()
}
複製代碼

class限制協議只用在class中,NSObjectProtocol限制只用在NSObject中,明顯class的範圍更廣,平常開發中均可以使用。

@synchronized


在ObjC平常開發中, @synchronized接觸會比較多,這個關鍵字能夠用來修飾一個變量,併爲其自動加上和解除互斥鎖,用以保證變量在做用範圍內不會被其餘線程改變。

但不幸的是Swift 中它已經不存在了。其實 @synchronized 在幕後作的事情是調用了 objc_sync 中的 objc_sync_enterobjc_sync_exit 方法,而且加入了一些異常判斷。所以,在 Swift 中,若是咱們忽略掉那些異常的話,咱們想要 lock 一個變量的話,能夠這樣寫:

private var isResponse: Bool {
    get {
        objc_sync_enter(lockObj)
        let result = _isResponse
        objc_sync_exit(lockObj)
        return result
    }

    set {
        objc_sync_enter(lockObj)
        _isResponse = newValue
        objc_sync_exit(lockObj)
    }
}
複製代碼

字面量


所謂字面量,就是指像特定的數字,字符串或者是布爾值這樣,可以直截了當地指出本身的類型併爲變量進行賦值的值。好比在下面:

let aNumber = 3
let aString = "Hello"
let aBool = true
複製代碼

在開發中咱們可能會遇到下面這種狀況:

public struct Thermometer {
    var temperature: Double
    public init(temperature: Double) {
        self.temperature = temperature
    }
}
複製代碼

想要建立一個Thermometer對象,可使用以下代碼:

let t: Thermometer = Thermometer(temperature: 20.0)
複製代碼

可是實際上Thermometer的初始化僅僅只須要一個Double類型的基礎數據,若是能經過字面量來賦值該多好,好比:

let t: Thermometer = 20.0
複製代碼

其實Swift 爲咱們提供了一組很是有意思的接口,用來將字面量轉換爲特定的類型。對於那些實現了字面量轉換接口的類型,在提供字面量賦值的時候,就能夠簡單地按照接口方法中定義的規則「無縫對應」地經過賦值的方式將值轉換爲對應類型。這些接口包括了各個原生的字面量,在實際開發中咱們常常可能用到的有:

  • ExpressibleByNilLiteral
  • ExpressibleByIntegerLiteral
  • ExpressibleByFloatLiteral
  • ExpressibleByBooleanLiteral
  • ExpressibleByStringLiteral
  • ExpressibleByArrayLiteral
  • ExpressibleByDictionaryLiteral

這樣,咱們就能夠實現剛纔的設想啦:

extension Thermometer: ExpressibleByFloatLiteral {
    public typealias FloatLiteralType = Double

    public init(floatLiteral value: Self.FloatLiteralType) {
        self.temperature = value
    }
}

let t: Thermometer = 20.0
複製代碼

struct與class


  • 共同點
  1. 定義屬性用於存儲值
  2. 定義方法用於提供功能
  3. 定義下標操做使得能夠經過下標語法來訪問實例所包含的值
  4. 定義構造器用於生成初始化值
  5. 經過擴展以增長默認實現的功能
  6. 實現協議以提供某種標準功能
  • 類更強大
  1. 繼承容許一個類繼承另外一個類的特徵
  2. 類型轉換容許在運行時檢查和解釋一個類實例的類型
  3. 析構器容許一個類實例釋聽任何其所被分配的資源
  4. 引用計數容許對一個類的屢次引用
  • 二者的區別
  1. struct是值類型,class是引用類型。
  2. struct有一個自動生成的成員逐一構造器,用於初始化新結構體實例中成員的屬性;而class沒有。
  3. struct中修改 self 或其屬性的方法必須將該實例方法標註爲 mutating;而class並不須要。
  4. struct不能夠繼承,class能夠繼承。
  5. struct賦值是值拷貝,拷貝的是內容;class是引用拷貝,拷貝的是指針。
  6. struct是自動線程安全的;而class不是。
  7. struct存儲在stack中,class存儲在heap中,struct更快。
  • 如何選擇?

通常的建議是使用最小的工具來完成你的目標,若是struct可以徹底知足你的預期要求,能夠多使用struct

柯里化 (Currying)


Currying就是把接受多個參數的方法進行一些變形,使其更加靈活的方法。函數式的編程思想貫穿於 Swift 中,而函數的柯里化正是這門語言函數式特色的重要表現。

例若有這樣的一個題目:實現一個函數,輸入是任一整數,輸出要返回輸入的整數 + 2。通常的實現爲:

func addTwo(_ num: Int) -> Int {
    return num + 2
}
複製代碼

若是實現+3,+4,+5呢?是否須要將上面的函數依次增長一遍?咱們其實能夠定義一個通用的函數,它將接受須要與輸入數字相加的數,並返回一個函數:

func add(_ num: Int) -> (Int) -> Int {
    return { (val) in
        return val + num
    }
}

let addTwo = add(2)
let addThree = add(3)
print("\(addTwo(1)) \(addThree(1))")
複製代碼

這樣咱們就能夠經過Curring來輸出模版來避免寫重複方法,從而達到量產類似方法的目的。

Swift 中定義常量和 Objective-C 中定義常量有什麼區別?


Swift中使用let關鍵字來定義常量,let只是標識這是一個常量,它是在runtime時肯定的,此後沒法再修改;ObjC中使用const關鍵字來定義常量,在編譯時或者編譯解析時就須要肯定值。

不經過繼承,代碼複用(共享)的方式有哪些?


  • 全局函數
  • 擴展

實現一個 min 函數,返回兩個元素較小的元素


func min<T : Comparable>(_ a : T , b : T) -> T {
    return a < b ? a : b
}
複製代碼

兩個元素交換


常見的一種寫法是:

func swap<T>(_ a: inout T, _ b: inout T) {
    let tempA = a
    a = b
    b = tempA
}
複製代碼

但若是使用多元組的話,咱們能夠這麼寫:

func swap<T>(_ a: inout T, _ b: inout T) {
    (a, b) = (b, a)
}
複製代碼

這樣一下變得簡潔起來,而且沒有顯示的增長額外空間

爲何要說沒有顯示的增長額外空間呢?
喵神在說多元組交換時,提到了沒有使用額外空間。但有一些開發者認爲多元組交換將會複製兩個值,致使多餘內存消耗,這種說法有些道理,但蝸牛並無找到實質性的證據,若是有同窗瞭解,能夠評論補充。
另外蝸牛在[Leetcode-交換數字](https://leetcode-cn.com/problems/swap-numbers-lcci/)中測試了兩種寫法的執行耗時和內存消耗,基本上多元組交換執行速度上要優於普通交換,但前者的內存消耗要更高些,感興趣的同窗能夠試試。
複製代碼

map與flatmap


都會對數組中的每個元素調用一次閉包函數,並返回該元素所映射的值,最終返回一個新數組。但flatmap更進一步,多作了一些事情:

  1. 返回的結果中會去除nil,而且會解包Optional類型。
  2. 會將N維數組變成1維數組返回。

defer


defer 所聲明的 block 會在當前代碼執行退出後被調用。正由於它提供了一種延時調用的方式,因此通常會被用來作資源釋放或者銷燬,這在某個函數有多個返回出口的時候特別有用。

func testDefer() {
    print("開始持有資源")
    defer {
        print("結束持有資源")
    }

    print("程序運行ing")
}

// 開始持有資源
// 程序運行ing
// 結束持有資源
複製代碼

使用defer會方便的將先後必要邏輯放在一塊兒,加強可讀性和維護,可是不正確的使用也會致使問題。例如上面的例子,在持有以前先判斷是否資源已經被其餘持有:

func testDefer(isLock: Bool) {
    if !isLock {
        print("開始持有資源")
        defer {
            print("結束持有資源")
        }
    }

    print("程序運行ing")
}

// 開始持有資源
// 結束持有資源
// 程序運行ing
複製代碼

咱們要注意到defer的做用域不是整個函數,而是當前的scope。那若是有多份defer呢?

func testDefer() {
    print("開始持有資源")

    defer {
        print("結束持有資源A")
    }

    defer {
        print("結束持有資源B")
    }

    print("程序運行ing")
}

// 開始持有資源
// 程序運行ing
// 結束持有資源B
// 結束持有資源A
複製代碼

當有多個defer時,後加入的先執行,能夠猜想Swift使用了stack來管理defer

String和NSString 的關係與區別


String是Swift類型,NSStringFoundation中的類,二者能夠無縫轉換。String是值類型,NSString是引用類型,前者更切合字符串的 "不變" 這一特性,而且值類型是自動多線程安全的,在使用上性能也有提高。

除非須要一些NSString特有的方法,不然使用String便可。

怎麼獲取一個String的長度?


僅僅是獲取字符串的字符個數,可使用count直接獲取:

let str = "Hello你好"
print("\(str.count)") // 7
複製代碼

若是想獲取字符串佔用的字節數,能夠根據具體的編碼環境來獲取:

print("\(str.lengthOfBytes(using: .utf8))")    // 11
print("\(str.lengthOfBytes(using: .unicode))") // 14
複製代碼

### [1, 2, 3].map{ $0 * 2 }都用了哪些語法糖


  1. [1, 2, 3]使用了字面量初始化,Array實現了ExpressibleByArrayLiteral協議。
  2. 使用了尾隨閉包。
  3. 未顯式聲明參數列表和返回值,使用了閉包類型的自動推斷。
  4. 閉包只有一句代碼時,可省略return,自動將這一句的結果做爲返回值。
  5. $0在未顯式聲明參數列表時,表明第一個參數,以此類推。

下面的代碼可否正常運行?結果是什麼?


var mutableArray = [1,2,3]
for i in mutableArray {
    mutableArray.removeAll()
    print("\(i)")
}
print(mutableArray)
複製代碼

能夠正常運行,結果以下:

1
2
3
[]
複製代碼

爲何會調用三次?

由於Array是個值類型,它是寫時賦值的,循環中mutableArray值一旦改變,for in上的mutableArray會產生拷貝,後者的值仍然是[1, 2, 3],所以會循環三次。

原創不易,文章有任何錯誤,歡迎批(feng)評(kuang)指(diao)教(wo),順手點個贊👍,不甚感激!
相關文章
相關標籤/搜索