大多數 Swift 開發人員不知道他們正在犯的錯誤
已發表: 2022-03-11來自 Objective-C 的背景,一開始,我覺得 Swift 阻礙了我。 Swift 不允許我取得進展,因為它的強類型特性有時會讓人憤怒。
與 Objective-C 不同,Swift 在編譯時強制執行許多要求。 在 Objective-C 中放寬的東西,例如id
類型和隱式轉換,在 Swift 中是沒有的。 即使您有一個Int
和一個Double
,並且想要將它們相加,您也必須將它們顯式地轉換為單一類型。
此外,可選項是語言的基本組成部分,即使它們是一個簡單的概念,也需要一些時間來適應它們。
一開始,您可能想強制展開所有內容,但這最終會導致崩潰。 當您熟悉該語言時,您會開始喜歡幾乎沒有運行時錯誤的方式,因為在編譯時會發現許多錯誤。
大多數 Swift 程序員之前都有豐富的 Objective-C 經驗,除其他外,這可能會導致他們使用他們熟悉的其他語言相同的實踐來編寫 Swift 代碼。 這可能會導致一些嚴重的錯誤。
在本文中,我們概述了 Swift 開發人員最常犯的錯誤以及避免這些錯誤的方法。
1. 強制展開選項
可選類型的變量(例如String?
)可能包含值,也可能不包含值。 當它們不持有值時,它們等於nil
。 要獲得可選項的值,首先您必須解開它們,這可以通過兩種不同的方式進行。
一種方法是使用if let
或guard let
進行可選綁定,即:
var optionalString: String? //... if let s = optionalString { // if optionalString is not nil, the test evaluates to // true and s now contains the value of optionalString } else { // otherwise optionalString is nil and the if condition evaluates to false }
其次是使用!
運算符,或使用隱式展開的可選類型(例如String!
)。 如果可選項為nil
,則強制展開將導致運行時錯誤並終止應用程序。 此外,嘗試訪問隱式展開的可選項的值也會導致相同的結果。
我們有時會在類/結構初始化程序中存在無法(或不想)初始化的變量。 因此,我們必須將它們聲明為可選項。 在某些情況下,我們假設它們在代碼的某些部分不會為nil
,因此我們強制解包它們或將它們聲明為隱式解包的可選項,因為這比必須始終進行可選綁定更容易。 這應該小心翼翼地完成。
這類似於使用IBOutlet
,它們是引用 nib 或情節提要中的對象的變量。 它們不會在父對像初始化時初始化(通常是視圖控制器或自定義UIView
),但我們可以確定當viewDidLoad
(在視圖控制器中)或awakeFromNib
(在視圖中)被調用時,它們不會為nil
,所以我們可以安全地訪問它們。
通常,最佳實踐是避免強制展開並使用隱式展開的選項。 始終考慮可選項可以是nil
並使用可選綁定適當地處理它,或者在強制解包之前檢查它是否不是nil
,或者在隱式解包選項的情況下訪問變量。
2. 不知道強參考週期的陷阱
當一對對象相互保持強引用時,就存在強引用循環。 這對 Swift 來說並不是什麼新鮮事,因為 Objective-C 也有同樣的問題,而經驗豐富的 Objective-C 開發人員應該能夠妥善處理這個問題。 重要的是要注意強引用以及什麼引用什麼。 Swift 文檔有一個專門討論這個主題的部分。
使用閉包時管理引用尤為重要。 默認情況下,閉包(或塊)保持對其中引用的每個對象的強引用。 如果這些對像中的任何一個對閉包本身有強引用,我們就有一個強引用循環。 有必要使用捕獲列表來正確管理您的引用是如何捕獲的。
如果塊捕獲的實例有可能在塊被調用之前被釋放,則必須將其捕獲為弱引用,這將是可選的,因為它可以是nil
。 現在,如果您確定捕獲的實例在塊的生命週期內不會被釋放,您可以將其捕獲為無主引用。 使用unowned
而不是weak
的優點是引用不是可選的,您可以直接使用該值而無需打開它。
在下面的示例中,您可以在 Xcode Playground 中運行, Container
類有一個數組和一個可選的閉包,只要它的數組發生變化就會調用它(它使用屬性觀察器來執行此操作)。 Whatever
類有一個Container
實例,在它的初始化器中,它為arrayDidChange
分配了一個閉包,並且這個閉包引用了self
,從而在Whatever
實例和閉包之間建立了牢固的關係。
struct Container<T> { var array: [T] = [] { didSet { arrayDidChange?(array: array) } } var arrayDidChange: ((array: [T]) -> Void)? } class Whatever { var container: Container<String> init() { container = Container<String>() container.arrayDidChange = { array in self.f(array) } } deinit { print("deinit whatever") } func f(s: [String]) { print(s) } } var w: Whatever! = Whatever() // ... w = nil
如果你運行這個例子,你會注意到deinit whatever
永遠不會被打印出來,這意味著我們的實例w
不會從內存中釋放。 為了解決這個問題,我們必須使用一個捕獲列表來不強烈地捕獲self
:
struct Container<T> { var array: [T] = [] { didSet { arrayDidChange?(array: array) } } var arrayDidChange: ((array: [T]) -> Void)? } class Whatever { var container: Container<String> init() { container = Container<String>() container.arrayDidChange = { [unowned self] array in self.f(array) } } deinit { print("deinit whatever") } func f(s: [String]) { print(s) } } var w: Whatever! = Whatever() // ... w = nil
在這種情況下,我們可以使用unowned
,因為self
在閉包的生命週期內永遠不會為nil
。
幾乎總是使用捕獲列表來避免引用循環是一種很好的做法,這將減少內存洩漏,並最終獲得更安全的代碼。
3. 到處使用self
與 Objective-C 不同,在 Swift 中,我們不需要使用self
來訪問方法內的類或結構的屬性。 我們只需要在閉包中這樣做,因為它需要捕獲self
。 在不需要的地方使用self
並不是一個錯誤,它工作得很好,並且不會有錯誤和警告。 但是,為什麼要編寫比您必須編寫的更多的代碼呢? 此外,保持代碼一致也很重要。
4.不知道你的類型的類型
Swift 使用值類型和引用類型。 此外,值類型的實例表現出與引用類型的實例略有不同的行為。 不知道每個實例適合的類別會導致對代碼行為的錯誤預期。
在大多數面向對象的語言中,當我們創建一個類的實例並將其傳遞給其他實例並作為方法的參數時,我們希望這個實例在任何地方都是相同的。 這意味著對它的任何更改都會在任何地方反映出來,因為事實上,我們所擁有的只是對完全相同數據的一堆引用。 表現出這種行為的對像是引用類型,在 Swift 中,所有聲明為class
的類型都是引用類型。
接下來,我們有使用struct
或enum
聲明的值類型。 值類型在分配給變量或作為參數傳遞給函數或方法時被複製。 如果您在復制的實例中更改某些內容,則不會修改原始實例。 值類型是不可變的。 如果您將新值分配給值類型實例的屬性,例如CGPoint
或CGSize
,則會使用更改創建一個新實例。 這就是為什麼我們可以在數組上使用屬性觀察器(如上面Container
類中的示例)來通知我們更改。 實際發生的是,隨著更改創建了一個新數組; 它被分配給屬性,然後didSet
被調用。
因此,如果您不知道您正在處理的對像是引用類型還是值類型,那麼您對代碼將要做什麼的期望可能是完全錯誤的。
5. 沒有充分利用枚舉的潛力
當我們談論枚舉時,我們通常會想到基本的 C 枚舉,它只是一個相關常量的列表,下面是整數。 在 Swift 中,枚舉更強大。 例如,您可以為每個枚舉案例附加一個值。 枚舉還具有方法和只讀/計算屬性,可用於為每個案例提供更多信息和細節。
枚舉的官方文檔非常直觀,錯誤處理文檔展示了枚舉在 Swift 中的額外功能的一些用例。 此外,請查看對 Swift 中枚舉的廣泛探索,以了解您可以用它們做的幾乎所有事情。

6.不使用功能特性
Swift 標準庫提供了許多函數式編程的基礎方法,讓我們只需一行代碼就可以做很多事情,例如 map、reduce 和 filter 等。
讓我們來看看幾個例子。
比如說,你必須計算一個表格視圖的高度。 假設您有一個UITableViewCell
子類,如下所示:
class CustomCell: UITableViewCell { // Sets up the cell with the given model object (to be used in tableView:cellForRowAtIndexPath:) func configureWithModel(model: Model) // Returns the height of a cell for the given model object (to be used in tableView:heightForRowAtIndexPath:) class func heightForModel(model: Model) -> CGFloat }
考慮一下,我們有一個模型實例數組modelArray
; 我們可以用一行代碼計算表格視圖的高度:
let tableHeight = modelArray.map { CustomCell.heightForModel($0) }.reduce(0, combine: +)
該map
將輸出一個CGFloat
數組,其中包含每個單元格的高度, reduce
會將它們相加。
如果要從數組中刪除元素,最終可能會執行以下操作:
var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } for s in supercars { if !isSupercar(s), let i = supercars.indexOf(s) { supercars.removeAtIndex(i) } }
這個例子看起來並不優雅,也不是很高效,因為我們為每個項目調用indexOf
。 考慮以下示例:
var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } for (i, s) in supercars.enumerate().reverse() { // reverse to remove from end to beginning if !isSupercar(s) { supercars.removeAtIndex(i) } }
現在,代碼效率更高,但可以通過使用filter
進一步改進:
var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"] func isSupercar(s: String) -> Bool { return s.characters.count > 7 } supercars = supercars.filter(isSupercar)
下一個示例說明瞭如何刪除UIView
的所有滿足特定條件的子視圖,例如與特定矩形相交的框架。 你可以使用類似的東西:
for v in view.subviews { if CGRectIntersectsRect(v.frame, rect) { v.removeFromSuperview() } } ``` We can do that in one line using `filter` ``` view.subviews.filter { CGRectIntersectsRect($0.frame, rect) }.forEach { $0.removeFromSuperview() }
但是,我們必須小心,因為您可能會想鏈接對這些方法的幾個調用來創建花哨的過濾和轉換,這可能會導致一行無法閱讀的意大利麵條代碼。
7. 留在舒適區,不要嘗試面向協議的編程
Swift 號稱是第一個面向協議的編程語言,正如 WWDC 面向協議的編程在 Swift 會議中所提到的。 基本上,這意味著我們可以圍繞協議對程序進行建模,並通過遵循協議並擴展它們來為類型添加行為。 例如,假設我們有一個Shape
協議,我們可以擴展CollectionType
(它符合Array
、 Set
、 Dictionary
等類型),並為其添加一個方法來計算交叉點的總面積
protocol Shape { var area: Float { get } func intersect(shape: Shape) -> Shape? } extension CollectionType where Generator.Element: Shape { func totalArea() -> Float { let area = self.reduce(0) { (a: Float, e: Shape) -> Float in return a + e.area } return area - intersectionArea() } func intersectionArea() -> Float { /*___*/ } }
語句where Generator.Element: Shape
是一個約束,它聲明擴展中的方法將僅在符合CollectionType
的類型的實例中可用,其中包含符合Shape
類型的元素。 例如,這些方法可以在Array<Shape>
的實例上調用,但不能在Array<String>
的實例上調用。 如果我們有一個符合Shape
協議的Polygon
類,那麼這些方法也可用於Array<Polygon>
的實例。
使用協議擴展,您可以為協議中聲明的方法提供默認實現,然後可以在符合該協議的所有類型中使用,而無需對這些類型(類、結構或枚舉)進行任何更改。 這在整個 Swift 標準庫中廣泛完成,例如, map
和reduce
定義在CollectionType
的擴展中,並且相同的實現由Array
和Dictionary
等類型共享,無需任何額外代碼。
這種行為類似於來自其他語言的mixin ,例如 Ruby 或 Python。 通過簡單地遵循具有默認方法實現的協議,您可以為您的類型添加功能。
面向協議的編程乍一看可能看起來很笨拙,也不是很有用,這可能會讓你忽略它,甚至不去嘗試。 這篇文章很好地掌握了在實際應用程序中使用面向協議的編程。
正如我們所了解的,Swift 不是一種玩具語言
斯威夫特最初受到很多懷疑。 人們似乎認為蘋果會用一種兒童玩具語言或非程序員的東西來取代 Objective-C。 然而,事實證明 Swift 是一種嚴肅而強大的語言,它使編程變得非常愉快。 由於它是強類型的,所以很難出錯,因此很難列出你在使用該語言時可能犯的錯誤。
當你習慣了 Swift 並回到 Objective-C 時,你會注意到其中的不同。 你會錯過 Swift 提供的不錯的功能,並且必須在 Objective-C 中編寫乏味的代碼才能達到相同的效果。 其他時候,您將面臨 Swift 在編譯期間捕獲的運行時錯誤。 對於 Apple 程序員來說,這是一次很棒的升級,隨著語言的成熟,還有很多東西要等。