Swift程式設計的15個技巧

孫薇發表於2016-03-07

相對於Objective-C,Swift是一種編譯程式碼時速度更快、安全性與可靠性更高、同時具有可預測性的語言。下面我們列出了在實踐中使用這種新語言時,所獲取一些Swift使用技巧。這些技巧有助於讓開發者編寫出更乾淨的程式碼,並能幫助更熟悉Objective-C的程式設計師適應Swift程式設計,同時適用於在Swift上具有各種背景經歷的人,請繼續往下看。

章節的順序是按照使用者對Swift的熟悉程度來排列的。第一部分是針對不太瞭解Swift的人,第二部分是針對初級入門者,而最後一部分是對於已在使用Swift的人。

你應當瞭解,但有可能不知道的Swift技巧

提高常數的可讀性

在Swift中使用struct的簡潔辦法,就是在應用中製作一個適用所有常數的檔案。由於Swift允許我們嵌用下面的結構,這種辦法非常有用:

import Foundation  
struct Constants {  
    struct FoursquareApi {  
        static let BaseUrl = "https://api.foursquare.com/v2/"  
    }      
    struct TwitterApi {  
        static let BaseUrl = "https://api.twitter.com/1.1/"  
    }  
    struct Configuration {  
        static let UseWorkaround = true  
    }      
}

巢狀讓我們可以為常數生成一個名稱空間(namespace)。例如:我們可以使用Constants.FoursquareApi.BaseUrl來訪問Foursquare的BaseUrl常數,這樣會使得資料可讀性更高,併為相關的常數提供一系列封裝。

為了提高效能,要避免NSObject與@objc

Swift允許我們將分類進行擴充套件,從NSObject到獲取物件的Objective-Cruntime系統功能。還允許我們用@objc來註釋Swift方法,以便在Objective-C runtime中使用。

支援Objective-C runtime,代表著系統不再通過通過靜態或vtable分配,而是動態分配來呼叫方法。結果就是:在呼叫支援Objective-C執行的方法時,效能損失會高達四倍。在實際應用中,這種情況對效能的影響也許微不足道,不過這樣一來,我們就知道通過Swift執行方法呼叫要比使用Objective-C快四倍。

在Swift中使用方法調配(Method Swizzling)

方法調配是替換一個已存在的方法實現。如果對此不熟悉,可以閱讀這篇文章。Swift優化後,不再像Objective-C中那樣,在runtime尋找方法的位置,而是直接呼叫記憶體地址。因此預設情況下,在Swift類中調配無法起效,除非:

  • 用動態關鍵字禁用這種優化。這是最佳選擇,如果資料庫完全以Swift構建的話,這種選擇也是最合理的方式。
  • 擴充套件NSObject。如果單純為了方法調配的話,不要用這種方式(而要採用動態的)。需要了解:在將NSObject作為基礎類的已存在類中,方法調配是有效的,不過最好使用動態選擇的方法。
  • 在要調配的方法中使用@objc註釋。如果我們想要調配的方法同時也需要使用Objective-C的程式碼,那麼這種方法是最合適的。

引用

更新:根據要求,我們增加了一個完全使用Swift的呼叫樣例。在這個樣例中仍需要Objective-C runtime,不過類並非繼承自NSObject,方法也未標記成@objc。

import UIKit  
class AwesomeClass {  
    dynamic func originalFunction() -> String {  
        return "originalFunction"  
    }    
    dynamic func swizzledFunction() -> String {  
        return "swizzledFunction"  
    }  
}  
let awesomeObject = AwesomeClass()  
print(awesomeObject.originalFunction()) // prints: "originalFunction"  
let aClass = AwesomeClass.self  
let originalMethod = class_getInstanceMethod(aClass, "originalFunction")  
let swizzledMethod = class_getInstanceMethod(aClass, "swizzledFunction")  
method_exchangeImplementations(originalMethod, swizzledMethod)  
print(awesomeObject.originalFunction())  // prints: "swizzledFunction"

入門者所需的Swift技巧

清理非同步程式碼

Swift在編寫補齊函式(completion function)上語法非常簡潔。在Objective-C中有completion block,不過出現的很晚,語法也有些粗糙,如下:

[self loginViaHttpWithRequest:request completionBlockWithSuccess:^(LoginOperation *operation, id responseObject) {  
  [self showMainScreen];  
} failure:^(LoginOperation *operation, NSError *error) {  
  [self showFailedLogin];  
}];

在Swift中有一種更簡單的新型閉包語法。任何將閉包作為末尾引數的方法都可以使用Swift的新語法,讓回撥更簡潔,如下:

loginViaHttp(request) { response in  
  if response.success {  
    showMainScreen()  
  } else {  
    showFailedLogin()  
  }  
}

控制對程式碼的訪問

應該堅持用合適的訪問控制修飾符(access control modifier)來封裝程式碼。如果封裝的好,無需記下思維過程,也無需詢問程式碼編寫者,就能理解這段程式碼是如何互動的。

Swift常見的訪問控制機制有三種:私人訪問、內部訪問和公共訪問。不過Swift中並沒有常見於其它面嚮物件語言中的protected訪問控制修飾符。為什麼會這樣呢?那是因為在子類中通過新的公共方法或屬性,就可以顯示protected方法或屬性,因此實際上保護是無效的。而且由於從任何地方都能重寫,因此protected並未給Swift編譯器開啟優化的機會。最後,由於protected阻止子類helper訪問子類能夠訪問的資訊,會讓封裝變差。想要了解Swift團隊關於protected更多的想法,請點選這裡檢視。

實地實驗與驗證

Playground是蘋果在2014年隨Swift一起推出的一款互動式程式設計工具,可以用來測試及驗證想法、學習Swift、與其他人分享概念。無需建立新專案,只需在執行Xcode的時候將playground選中就可以了。

也可以在Xcode中建立新的playground:

一旦有了playground,在程式設計時便能實時看到結果:

通過Playground可以將想法原型化,並以程式碼形式展示,同時還不會造成開啟新專案的額外開銷。


安全地使用可選值

可選值(optional)屬性指的是這個屬性或有效值或無值(為空)。通過可選值的名稱+感嘆號,格式為optionalProperty!,便可隱式解開一個可選值。 一般這是需要避免的,因為感嘆號暗示著“危險”。

不過有些情況下,隱式解開可選值是可以接受的。比如IBOutlets就是預設將可選值隱式解開的(在Interface Builder中點選拖拽時),因為UIKit假定我們是將物件介面(outlet)與IB連線起來的。IBOutlets在初始化之後已經設定好了,因此介面是可選值的,同時根據Swift規則,在初始化之後所有非可選值的屬性必須有值。另一個通過名稱獲得UIImage的案例是存在於我們的asset catalog之中的:

let imageViewSavvyNewYearsParty = UIImageView(image: UIImage(named: "Savvy2016.png")!)

將預設值設定為常量屬性,在不隱式開啟可選值的情況下是無法做到的。也就是說,!仍舊代表“危險!”但在這種情況下,是告知我們需要當心錯誤,並在執行前驗證名稱是否相符。一般來講,假如我們必須使用空值,app就會有崩潰的風險。用!來隱式開啟值會讓編譯器知道,我們已經知道在執行時可選值不會為空。在幾乎所有場景之中,這都是帶有賭 博性質的,因此最好使用if let模式來確定可選值是有有效值還是為空:

if let name = user.name {  
    print(name)  
} else {  
    print("404 Name Not Found")  
}

拋棄數字物件(NSNumber)

Objective-C使用C primitives來代表數字,用Foundation Objective-C API來提供數字物件型別,將primitives裝箱拆箱。需要在primitives與物件型別之間切換時,程式碼會像 [array addObject:@(intPrimitive)]和[array[0] intValue]這樣。Swift就不會有這種不當的機制。相對的,我們實際上可以向Swift字典和陣列中新增Int / Float / AnyObject值。

下面是代替數字物件的一些Swift最常用的型別:

  • Swift: Objective-C
  • Int: [NSNumber integerValue]
  • UInt: [NSNumber unsignedIntegerValue]
  • Float: [NSNumber floatValue]
  • Bool: [NSNumber boolValue]
  • Double: [NSNumber doubleValue]

在用Objective-C編寫的不同型別中,我們仍可以用數字物件來進行轉換,不過在Swift中,轉化值的常用方式是使用目標型別的建構函式。舉個例子,如果我們從API中獲得一個數字userID,將其在數字物件中開啟並顯示為字串,在Objective-C中需要輸入[userId stringValue]。而在Swift中數字物件不再使用(除非要向後相容Objective-C),因為在Swift中,數字結構與在Objective-C中限制不同。
引用

注意:在使用Objective-C或依賴沒有Swift封裝的舊式程式碼庫中,可能仍得使用數字物件。在這種情況下,數字物件API基本沒什麼變化。

相反,在Swift中我們通過建構函式進行等效轉換。舉個例子,如果userID是一個Int,而我們想要字串的話,只需通過String(userId)進行轉換。這比一直將數字物件裝箱拆箱容易多了,不過數字物件所提供的各種各樣的轉換,確實讓API簡單易用。

通過預設引數減少樣板檔案程式碼

在Swift中,函式自變數現在可以有預設值了。這些預設的引數減少了雜亂程度。如果某函式的被呼叫者選擇使用預設值,由於預設引數可以省略,這個函式呼叫就能更短一些了。例如:

unc printAlertWithMessage(message: String, title: String = "title") {  
   print("Alert: \(title) | \(message)")  
}  
printAlertWithMessage("message") // prints: Alert: title | message  
printAlertWithMessage("message", title: "non-default title") // prints: Alert: non-default title | messagex

為更熟練的使用者提供的一些Swift技巧

通過Guard來驗證方法

Swift的guard語句讓程式碼更簡潔、更安全。guard語句會檢查一到多個情況,找出不符合else部分的呼叫。而else部分需要return,break,continue或throw語句來終止方法的執行,也就是說終止程式控制的執行。

我們使用guard語句來減少程式碼混亂,並避免在if/else語句中的嵌入。由於在guard語句的else部分中,程式碼必須轉移程式控制的範圍,如果出現無效的情況,簡單地採用if語句來呼叫return語句更為安全。在編譯時這些bug仍有可能出現。如果guard語句的情況通過的話,在我們的範圍中,解包後的可選值仍舊可用。

class ProjectManager {  

    func increaseProductivityOfDeveloper(developer: Developer) {  

        guard let developerName = developer.name else {  
            print("Papers, please!")  
            return  
        }  
        let slackMessage = SlackMessage(message: "\(developerName) is a great iOS Developer!")  
        slackMessage.send()  
    }  
}

用Defer管理程式控制流

defer語句會推遲包含這個命令的程式碼執行,直到當前範圍終止。也就是說,在defer語句中清理邏輯是可以替換的,而且只要離開相應的呼叫範圍,這段命令就肯定就會被呼叫。這樣可以減少冗餘步驟,更重要的是增加安全性。

func deferExample() {  
    defer {  
        print("Leaving scope, time to cleanup!")  
    }  
    print("Performing some operation...")  
}  

// Prints:  
// Performing some operation...  
// Leaving scope, time to cleanup!

簡化單例模式(Singleton)

在任何語言中對單例模式的使用都屬於熱議話題,不過它仍是大多數開發人員非常熟悉的模式。在Objective-C中,實現單例模式包括多個步驟,以便確保不會多次建立單例模式類。在Swift中這種使用有了大幅簡化。下面我們會看到在Objective-C中實現單例模式的程式碼行數,是在Swift中實現單例模式程式碼的兩倍。除此之外,由於使用了dispatch token,不僅可讀性較差,也很難記住。

Objective-C:

@implementation MySingletonClass  

    +(id)sharedInstance {  
        static MySingletonClass *sharedInstance = nil;  
        static dispatch_once_t onceToken;  
        dispatch_once(&onceToken, ^{  
            sharedInstance = [[self alloc] init];  
        });  
        return sharedInstance;  
    }

Swift:

class MySingletonClass {  
    static let sharedInstance = MySingletonClass()  
    private init() {  
    }  
}

通過協議擴充套件減少重複的程式碼

在Objective-C中,我們通過分類來擴充套件已有的型別,不過這種做法對協議無效。Swift允許向協議中新增功能,使用Swift可以擴充套件單協議(甚至在標準資料庫中的那些!),並將其應用在實現協議的類中。協議擴充套件足夠將我們的整個程式設計正規化從物件導向式改為面向協議式。這個概念的關鍵在於,我們預設通過協議來新增功能,而不是通過類,以便增加程式碼的可複用性。想要了解更多面向協議程式設計的知識,請檢視這個視訊,摘自WWDC 2015。

建立全域性Helper函式

全域性變數和函式經常被合稱為“壞東西”,不過事實是兩者都能讓程式碼更乾淨,真正的壞東西是全域性狀態。全域性函式經常需要全域性狀態來完成相關工作,因此很容易理解它們為什麼會有這樣的壞名聲。下面是一些Grand Central Dispatch的helper函式樣例,不是建立在全域性狀態之上,而且多少有些語法糖(指計算機語言中新增的某種語法,這種語法對語言的功能並沒有影響,但是更方便程式設計師使用)的性質。下面我們會採用dispatch_after函式,用Swift的方式來解包:

import Foundation  

/** 
    Executes the closure on the main queue after a set amount of seconds. 

    - parameter delay:   Delay in seconds 
    - parameter closure: Code to execute after delay 
*/  
func delayOnMainQueue(delay: Double, closure: ()->()) {  
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(delay * Double(NSEC_PER_SEC))), dispatch_get_main_queue(), closure)  
}  

/** 
    Executes the closure on a background queue after a set amount of seconds. 

    - parameter delay:   Delay in seconds 
    - parameter closure: Code to execute after delay 
*/  
func delayOnBackgroundQueue(delay: Double, closure: ()->()) {  
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(delay * Double(NSEC_PER_SEC))), dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), closure)  
}

下面是新解包的函式樣例:

delayOnBackgroundQueue(5) {  
    showView()  
}

下面是未解包的函式樣例:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(delay * Double(NSEC_PER_SEC))), dispatch_get_global_queue(QOS_CLASS_UTILITY, 0)) {  
    showView()  
}

用Swift語法來解包C函式,讓我們的程式碼更易於一眼理解。找到你最喜歡的函式,試一下吧!只要在正確方法命名上盡責,將來程式的維護者肯定感激我們。如果我們將上面的方法簽名修改為delay(delay: Double, closure: ()->()),這就成了不負責任的方法命名反例,因為dispatch_after需要GCD佇列,而從名稱中看不出來使用的哪個佇列。然而,如果我們使用的程式碼庫在主執行緒所有方法的執行上有既定規範,除非在名稱或評論上另有指示,delay(delay: Double, closure: ()->())就可以是一個正確的方法名稱。無論我們如何命名helper函式,它們都是為了通過包裝樣本程式碼節省時間,讓程式碼更易讀。

擴充套件集合效能

Swift增加了一些方法,幫助我們對集合進行簡潔的查詢和修改。這些集合方法受到了函式式語言的啟發。我們使用集合將多個值儲存到一個單獨的資料結構中,通常我們也會查詢和修改集合。這些函式是基於Swift的標準資料庫構建,協助簡化常見的任務。為了協助詮釋下面這些函式,我們使用了這些樣例:let ints = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]。

對集合中的每個值執行閉包對映(map),之後返回填充有對映值的對映結果型別陣列。下面我們將Int陣列轉化為字串資料:

let strings = ints.map { return String($0) }  
print("strings: \(strings)") // prints: strings: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]

對陣列中的每個值執行函式篩選(filter),返回Bool值。在結果陣列中,只會返回true值,而不會返回false值。下面我們從ints陣列中篩選奇數:

let evenInts = ints.filter { return ($0 % 2 == 0) }  
print("evenInts: \(evenInts)") // prints: evenInts: [0, 2, 4, 6, 8]

reduce比map和filter更復雜,不過因為非常有用,花時間學習也是有價值的。第一個引數就是第一個reduce值(在下面的案例中為0)。第二個引數是訪問之前reduce值和陣列現值的函式。在本例中,我們的函式是將之前的函式值簡單加到陣列的現值中。

let reducedInts = ints.reduce(0, combine: +)  
print("reducedInts: \(reducedInts)") // prints: reducedInts: 45  

// defined another way:   

let reducedIntsAlt = ints.reduce(0) { (previousValue: Int, currentValue: Int) -> Int in  
    return previousValue + currentValue  
}  
print("reducedIntsAlt: \(reducedIntsAlt)") // prints: reducedIntsAlt: 45

通過map,filter,reduce方面的技巧,就能減少篩選時和處理集合時的工作量,並增加可讀性,方便以後的人維護。

結論

這份列表來自於我們團隊的建議,收集了一些最常用的技巧,其中很多在整個程式碼庫中都很常見。隨著Swift這門程式語言的發展,像這樣的技巧也在繼續增加。我們希望能繼續看到Swift的變化,並期待在應用中更多地使用這種語言。

相關文章