作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Nilson Souto
Verified Expert in Engineering
13 Years of Experience

Nilson(拥有BCS/BScTech双重身份)从事iOS开发和2D/3D美术工作已有8年以上, 专注于物理和车辆模拟, games, and graphics.

Expertise

Share

基于Objective-C背景,一开始,我觉得Swift阻碍了我的发展. 由于Swift的强类型特性,它不允许我取得进展, 这有时会让人愤怒.

与Objective-C不同,Swift在编译时强制执行许多要求. 在Objective-C中放松的东西,比如 id 类型和隐式转换在Swift中是不存在的. Even if you have an Int and a Double,并且要将它们加起来,则必须显式地将它们转换为单个类型.

Also, 可选选项是语言的基本组成部分, 尽管它们是一个简单的概念, 需要一些时间来适应它们.

In the beginning, 您可能需要强制打开所有内容, 但这最终会导致崩溃. 当你熟悉了这门语言, 您开始喜欢几乎没有运行时错误,因为许多错误是在编译时捕获的.

Most Swift programmers 有使用Objective-C的丰富经验吗, which, among other things, 可能会引导他们使用他们在其他语言中熟悉的相同实践来编写Swift代码. 这可能会导致一些严重的错误.

In this article, 我们列出了Swift开发者最常犯的错误以及避免这些错误的方法.

别搞错了——Objective-C的最佳实践不是Swift的最佳实践.

1. Force-Unwrapping可选

可选类型的变量(如.g. String?)可能包含值,也可能不包含值. 当它们没有值时,它们等于 nil. 要获取可选参数的值,首先必须 unwrap 它们可以用两种不同的方式来制作.

一种方法是可选绑定 if let or a guard let, that is:

  var optionalString:字符串?
  //...
  如果让s = optionalString {
      //如果optionalString不为nil,测试结果为
      // true和s现在包含optionalString的值
  }
  else {
      //否则optionalString为nil, if条件求值为false
  }

命令强制展开 ! 操作符,或者使用隐式取消包装的可选类型(例如.g. String!). If the optional is nil,强制展开将导致运行时错误并终止应用程序. 此外,试图访问隐式未包装的可选项的值也会导致相同的结果.

有时在类/结构初始化器中有不能(或不想)初始化的变量. 因此,我们必须将它们声明为可选的. 在某些情况下,我们认为他们不会 nil 在我们代码的某些部分, 因此,我们强制展开它们或将它们声明为隐式展开的可选选项,因为这比必须一直进行可选绑定要容易得多. 这件事应该小心做.

这与使用 IBOutletS,它们是在nib或storyboard中引用对象的变量. 它们不会在父对象的初始化(通常是视图控制器或自定义)时被初始化 UIView),但我们可以肯定他们不会 nil when viewDidLoad (在视图控制器中)或 awakeFromNib (在视图中)被调用,因此我们可以安全地访问它们.

一般来说,最佳实践是避免强制展开并使用隐式展开可选项. 总是考虑可选的可能 nil 并适当地处理它,要么使用可选绑定,要么检查它是否不是 nil 在强制展开之前,或者在隐式展开可选变量的情况下访问变量之前.

2. 不知道强引用循环的陷阱

当一对对象彼此保持强引用时,就存在强引用循环. 这对斯威夫特来说并不新鲜, 因为Objective-C也有同样的问题, 经验丰富的Objective-C开发人员应该妥善管理这一点. 重要的是要注意强引用和什么引用什么. Swift文档中有一个 部分专门讨论这个主题.

在使用闭包时,管理引用尤为重要. By default, closures (or blocks), 保持对其中引用的每个对象的强引用. 如果这些对象中的任何一个对闭包本身有强引用, 我们有一个强参考循环. 有必要利用 capture lists 正确管理如何捕获您的引用.

如果有可能被块捕获的实例将在块被调用之前被释放, 你必须把它作为一个 weak reference,这将是可选的,因为它可以 nil. Now, 如果您确定捕获的实例在块的生命周期内不会被释放, 你可以把它作为一个 unowned reference. The advantage of using unowned instead of weak 是不是这个引用不是可选的,你可以直接使用这个值而不需要打开它.

在下面的例子中,你可以在Xcode Playground中运行 Container 类具有一个数组和一个可选闭包,该闭包在其数组更改时调用 property observers to do so). The Whatever class has a Container 实例,并在其初始化器中,将闭包分配给 arrayDidChange 这个闭包引用 self,从而在两者之间建立了牢固的关系 Whatever instance and the closure.

    struct Container {
        var array: [T] = [] {
            didSet {
                arrayDidChange?(array: array)
            }
        }

        var arrayDidChange: ((array: [T]) -> Void)?
    }

    class Whatever {
        var container: Container

        init() {
            container = Container()


            container.arrayDidChange ={数组中
                self.f(array)
            }
        }

        deinit {
            print("deinit whatever")
        }

        func f(s: [String]) {
            print(s)
        }
    }

    var w: Whatever! = Whatever()
    // ...
    w = nil

如果运行这个示例,您将注意到这一点 deinit whatever 永远不会被打印出来,也就是说我们的实例 w 不会从内存中释放. 为了解决这个问题,我们必须使用捕获列表来不捕获 self strongly:

    struct Container {
        var array: [T] = [] {
            didSet {
                arrayDidChange?(array: array)
            }
        }

        var arrayDidChange: ((array: [T]) -> Void)?
    }


    class Whatever {
        var container: Container

        init() {
            container = Container()

            container.arrayDidChange ={[无主的自我]数组
                self.f(array)
            }
        }

        deinit {
            print("deinit whatever")
        }

        func f(s: [String]) {
            print(s)
        }
    }

    var w: Whatever! = Whatever()
    // ...
    w = nil

In this case, we can use unowned, because self will never be nil 在闭包的生命周期内.

几乎总是使用捕获列表来避免引用循环是一种很好的做法, 这将减少内存泄漏, 最后是一个更安全的密码.

3. Using self Everywhere

与Objective-C不同,在Swift中,我们不需要使用 self 在方法中访问类或结构的属性. 我们只需要在闭包中这样做,因为它需要捕获 self. Using self 不需要的地方不是一个错误吗, it works just fine, 不会有错误,也不会有警告. 但是,为什么要编写比必须编写的代码更多呢? 此外,保持代码的一致性也很重要.

4. 不了解自己的类型

Swift uses value types and reference types. Moreover, 值类型的实例表现出与引用类型实例略有不同的行为. 如果不知道每个实例属于哪个类别,就会对代码的行为产生错误的期望.

在大多数面向对象的语言中, 当我们创建一个类的实例并将其传递给其他实例或作为方法的参数时, 我们期望这个实例在任何地方都是一样的. 这意味着对它的任何更改都会在任何地方反映出来, because in fact, 我们所拥有的只是一堆对完全相同数据的引用. 表现出这种行为的对象是引用类型,在Swift中,所有类型声明为 class are reference types.

接下来,我们有值类型声明使用 struct or enum. 值类型在赋值给变量或作为参数传递给函数或方法时被复制. 如果您更改了复制实例中的某些内容,原始实例将不会被修改. Value types are immutable. 如果将新值分配给值类型实例的属性,例如 CGPoint or CGSize,则使用更改创建一个新实例. 这就是为什么我们可以在数组上使用属性观察器(如上面的例子中 Container 类)将更改通知我们. 到底发生了什么, is that a new array is created with the changes; it is assigned to the property, and then didSet gets invoked.

Thus, 如果您不知道正在处理的对象是引用类型还是值类型, 您对代码将要做什么的期望, might be entirely wrong.

5. 没有充分利用枚举的潜力

When we talk about enums, 我们通常认为是基本的C枚举, 它只是一列相关的常量下面都是整数. 在Swift中,枚举要强大得多. 例如,可以为每个枚举用例附加一个值. 枚举还具有方法和只读/计算属性,可用于用更多信息和细节丰富每个案例.

The official documentation on enums 非常直观,而 错误处理文档 给出了枚举在Swift中额外功能的几个用例. 此外,请查看以下广泛的 Swift中枚举的探索 去学习你能用它们做的几乎所有事情.

6. 不使用功能特性

Swift标准库提供了许多函数式编程的基本方法,让我们只用一行代码就能做很多事情, such as map, reduce, and filter, among others.

让我们来看几个例子.

比如说,你需要计算表格视图的高度. Given you have a UITableViewCell 子类,如:

  类CustomCell: UITableViewCell {
      //使用给定的模型对象设置单元格(用于tableView:cellForRowAtIndexPath:)
      函数configureWithModel(model: model)
      //返回给定模型对象的单元格高度(用于tableView:heightForRowAtIndexPath:)
      class func heightForModel(model: Model) -> CGFloat
  }

考虑一下,我们有一个模型实例数组 modelArray; we can compute the height of the table view with one line of code:

  让tableHeight = modelArray.map { CustomCell.heightForModel($0) }.reduce(0, combine: +)

The map will output an array of CGFloat,包含每个单元格的高度 reduce will add them up.

如果你想从数组中删除元素,你可能会这样做:

  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),设i = supercars.indexOf(s) {
          supercars.removeAtIndex(i)
      }
  }

这个例子看起来并不优雅,也不是很有效,因为我们调用了 indexOf for each item. 考虑下面的例子:

  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(){//从结束到开始反转移除
      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 {
    如果CGRectIntersectsRect (v.frame, rect) {
      v.removeFromSuperview()
    }
  }
  ```
  我们可以在一行中使用filter来完成
  ```
  view.subviews.{CGRectIntersectsRect($0.frame, rect) }.forEach { $0.removeFromSuperview() }

We have to be careful, though, 因为您可能会试图将对这些方法的几个调用链接起来,以创建花哨的过滤和转换, 这可能会导致一行难以读懂的意大利面条式代码.

7. 呆在舒适区,不尝试面向协议的编程

斯威夫特被认为是第一个 面向协议的编程语言,正如WWDC中提到的 Swift中面向协议的编程 session. Basically, 这意味着我们可以围绕协议对程序进行建模,并通过遵循协议和扩展协议来为类型添加行为. 例如,已知a Shape protocol, we can extend CollectionType (这与诸如 Array, Set, Dictionary),并在其中添加一个计算占交点总面积的方法

  protocol Shape {
      var area: Float { get }
      func intersect(shape: Shape) -> Shape?
  }

  扩展CollectionType where Generator.Element: Shape {
      func totalArea() -> Float {
          let area = self.reduce(0) { (a: Float, e: Shape) -> Float in
              return a + e.area
          }

          返回区域- intersectionArea()
      }

      func intersectionArea() -> Float {
          /*___*/
      }
  }

The statement where Generator.Element: Shape 是否有约束声明扩展中的方法仅在符合以下类型的实例中可用 CollectionType,其中包含符合的类型的元素 Shape. 的实例上调用这些方法 Array,但不是在…的例子上 Array. If we have a class Polygon that conforms to the Shape 的实例,那么这些方法将可用于 Array as well.

With protocol extensions, 您可以为协议中声明的方法提供默认实现, 然后,它将在符合该协议的所有类型中可用,而无需对这些类型(类, structs or enums). 这在Swift标准库中被广泛使用,例如 mapand reduce 的扩展中定义的 CollectionType的类型共享相同的实现,例如 Array and Dictionary without any extra code.

此行为类似于 mixins 从其他语言,如Ruby或Python. 通过简单地遵循带有默认方法实现的协议, 向类型添加功能.

面向协议的编程乍一看可能很笨拙,也不是很有用, 这可能会让你忽略它,甚至不给它一个机会. This post 很好地掌握了在实际应用中使用面向协议的编程.

正如我们所知,Swift不是一门玩具语言

Swift was initially received with a lot of skepticism; people seemed to think that Apple was going to replace Objective-C with a toy language for kids or with something for non-programmers. 然而,Swift已经被证明是一门严肃而强大的语言,它让编程变得非常愉快. 因为它是强类型的, 犯错误是很难的, and as such, 很难列出你在使用这门语言时可能犯的错误.

当你习惯了Swift并回到Objective-C时,你会注意到其中的区别. 你将错过Swift提供的好功能,并且必须在Objective-C中编写乏味的代码来实现相同的效果. 其他时候,你会面临Swift在编译过程中捕获的运行时错误. 对于苹果程序员来说,这是一个巨大的升级, 随着语言的成熟,还有很多东西要做.

聘请Toptal这方面的专家.
Hire Now
Nilson Souto

Nilson Souto

Verified Expert in Engineering
13 Years of Experience

贝拉维斯塔,巴拿马城,巴拿马,巴拿马

2013年2月19日成为会员

About the author

Nilson(拥有BCS/BScTech双重身份)从事iOS开发和2D/3D美术工作已有8年以上, 专注于物理和车辆模拟, games, and graphics.

作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Expertise

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.