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

尼尔森Souto

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

专业知识

工作经验

13

分享

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

与Objective-C不同,斯威夫特在编译时强制执行许多要求. 在Objective-C中放松的东西,比如 id 类型和隐式转换在斯威夫特中是不存在的. 即使你有 Int 和一个 ,并且要将它们加起来,则必须显式地将它们转换为单个类型.

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

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

大多数 斯威夫特的程序员 有使用Objective-C的丰富经验吗, 哪一个。, 除此之外, 可能会引导他们使用他们在其他语言中熟悉的相同实践来编写斯威夫特代码. 这可能会导致一些严重的错误.

在本文中, 我们列出了斯威夫特开发者最常犯的错误以及避免这些错误的方法.

别搞错了——Objective-C的最佳实践不是斯威夫特的最佳实践.

1. Force-Unwrapping可选

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

一种方法是可选绑定 如果让 或者一个 ,即:

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

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

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

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

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

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

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

在使用闭包时,管理引用尤为重要. 默认情况下, 闭包(或块), 保持对其中引用的每个对象的强引用. 如果这些对象中的任何一个对闭包本身有强引用, 我们有一个强参考循环. 有必要利用 获取列表 正确管理如何捕获您的引用.

如果有可能被块捕获的实例将在块被调用之前被释放, 你必须把它作为一个 弱引用,这将是可选的,因为它可以 . 现在, 如果您确定捕获的实例在块的生命周期内不会被释放, 你可以把它作为一个 无主参考. 使用的优势 无主 而不是 是不是这个引用不是可选的,你可以直接使用这个值而不需要打开它.

在下面的例子中,你可以在Xcode Playground中运行 容器 类具有一个数组和一个可选闭包,该闭包在其数组更改时调用 财产观察员 这样做). 的 无论 类有一个 容器 实例,并在其初始化器中,将闭包分配给 arrayDidChange 这个闭包引用 自我,从而在两者之间建立了牢固的关系 无论 实例和闭包.

    结构体 容器 {
        var array: [T] = [] {
            did集 {
                arrayDidChange?(数组:数组)
            }
        }

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

    类无论 {
        var 容器: 容器<字符串>

        init () {
            容器 = 容器<字符串>()


            容器.arrayDidChange ={数组中
                自我.f(数组)
            }
        }

        deinit {
            打印(“deinit”)
        }

        函数f(s: [字符串]) {
            打印(s)
        }
    }

    var w:随便! = ()
    // ...
    W =零

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

    结构体 容器 {
        var array: [T] = [] {
            did集 {
                arrayDidChange?(数组:数组)
            }
        }

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


    类无论 {
        var 容器: 容器<字符串>

        init () {
            容器 = 容器<字符串>()

            容器.arrayDidChange ={[无主的自我]数组
                自我.f(数组)
            }
        }

        deinit {
            打印(“deinit”)
        }

        函数f(s: [字符串]) {
            打印(s)
        }
    }

    var w:随便! = ()
    // ...
    W =零

在这种情况下,我们可以使用 无主,因为 自我 永远不会 在闭包的生命周期内.

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

3. 使用 自我 到处都是

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

4. 不了解自己的类型

快速使用 值类型引用类型. 此外, 值类型的实例表现出与引用类型实例略有不同的行为. 如果不知道每个实例属于哪个类别,就会对代码的行为产生错误的期望.

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

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

因此, 如果您不知道正在处理的对象是引用类型还是值类型, 您对代码将要做什么的期望, 可能是完全错误的.

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

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

官方的 关于枚举的文档 非常直观,而 错误处理文档 给出了枚举在斯威夫特中额外功能的几个用例. 此外,请查看以下广泛的 斯威夫特中枚举的探索 去学习你能用它们做的几乎所有事情.

6. 不使用功能特性

斯威夫特标准库提供了许多函数式编程的基本方法,让我们只用一行代码就能做很多事情, 如 map, 减少, 过滤器除其他外.

让我们来看几个例子.

比如说,你需要计算表格视图的高度. 如果你有 UITableViewCell 子类,如:

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

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

  让tableHeight = model数组.map {CustomCell.heightForModel(0美元)}.减少(0,组合:+)

map 将输出一个数组 CGFloat,包含每个单元格的高度 减少 把它们加起来.

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

  var 超级跑车 = ["Lamborghini", “布加迪”, “AMG”, “阿尔法罗密欧”, “科尼赛克”, “保时捷”, “法拉利”, “麦克拉伦”, “阿巴特”, “摩根”, “卡特勒姆”, “劳斯莱斯”, “奥迪”)

  func isSupercar(s: 字符串) -> Bool {
      返回年代.字符.count > 7
  }

  对于超级跑车中的s {
      if !isSupercar(s),设i = 超级跑车.indexOf () {
          超级跑车.removeAtIndex(我)
      }
  }

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

  var 超级跑车 = ["Lamborghini", “布加迪”, “AMG”, “阿尔法罗密欧”, “科尼赛克”, “保时捷”, “法拉利”, “麦克拉伦”, “阿巴特”, “摩根”, “卡特勒姆”, “劳斯莱斯”, “奥迪”)

  func isSupercar(s: 字符串) -> Bool {
      返回年代.字符.count > 7
  }

  对于超级跑车中的(i, s).枚举erate ().Reverse(){//从结束到开始反转移除
      if !isSupercar (s) {
          超级跑车.removeAtIndex(我)
      }
  }

现在,代码更加高效了,但是还可以通过使用 过滤器:

  var 超级跑车 = ["Lamborghini", “布加迪”, “AMG”, “阿尔法罗密欧”, “科尼赛克”, “保时捷”, “法拉利”, “麦克拉伦”, “阿巴特”, “摩根”, “卡特勒姆”, “劳斯莱斯”, “奥迪”)

  func isSupercar(s: 字符串) -> Bool {
      返回年代.字符.count > 7
  }

  超级跑车.过滤器(isSupercar)

下一个示例说明如何删除类的所有子视图 UIView 满足一定的标准,比如框架与特定的矩形相交. 你可以这样说:

  对于视图中的v.子视图{
    如果CGRectIntersectsRect (v.框架,矩形){
      v.removeFromSuper视图 ()
    }
  }
  ```
  我们可以在一行中使用过滤器来完成
  ```
  视图.子视图.{CGRectIntersectsRect($0.框架,矩形)}.forEach {$0.removeFromSuper视图 ()}

我们必须小心, 虽然, 因为您可能会试图将对这些方法的几个调用链接起来,以创建花哨的过滤和转换, 这可能会导致一行难以读懂的意大利面条式代码.

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

斯威夫特被认为是第一个 面向协议的编程语言,正如WWDC中提到的 斯威夫特中面向协议的编程 会话. 基本上, 这意味着我们可以围绕协议对程序进行建模,并通过遵循协议和扩展协议来为类型添加行为. 例如,已知a 形状 协议,我们可以扩展 CollectionType (这与诸如 数组, , 字典),并在其中添加一个计算占交点总面积的方法

  形状{
      var区域:浮动{get}
      func intersect(shape: 形状) -> 形状?
  }

  扩展CollectionType 在发电机.元素:形状{
      func totalArea() -> Float {
          让面积=自我.减少(0) { (a: Float, e: 形状) -> Float in
              返回a + e.area
          }

          返回区域- intersectionArea()
      }

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

该声明 在发电机.元素:形状 是否有约束声明扩展中的方法仅在符合以下类型的实例中可用 CollectionType,其中包含符合的类型的元素 形状. 的实例上调用这些方法 数组<形状>,但不是在…的例子上 数组<字符串>. 如果我们上课 多边形 这符合 形状 的实例,那么这些方法将可用于 数组<多边形> 也。.

通过协议扩展, 您可以为协议中声明的方法提供默认实现, 然后,它将在符合该协议的所有类型中可用,而无需对这些类型(类, 结构体或枚举). 这在斯威夫特标准库中被广泛使用,例如 map减少 的扩展中定义的 CollectionType的类型共享相同的实现,例如 数组字典 没有任何额外的代码.

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

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

正如我们所知,斯威夫特不是一门玩具语言

斯威夫特 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. 然而,斯威夫特已经被证明是一门严肃而强大的语言,它让编程变得非常愉快. 因为它是强类型的, 犯错误是很难的, 因此, 很难列出你在使用这门语言时可能犯的错误.

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

就这一主题咨询作者或专家.
预约电话
尼尔森·索托的头像
尼尔森Souto

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

成员自 2013年2月19日

作者简介

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

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

专业知识

工作经验

13

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

订阅意味着同意我们的 隐私政策

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

订阅意味着同意我们的 隐私政策

Toptal开发者

加入总冠军® 社区.