大多数 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 程序员来说,这是一次很棒的升级,随着语言的成熟,还有很多东西要等。