个性化阅读
专注于IT技术分析

大多数Swift开发人员不知道自己犯的错误

本文概述

从Objective-C的背景开始, 我感觉就像Swift阻止了我。 Swift不允许我取得进展, 因为它的类型强烈, 这在过去常常令人发指。

与Objective-C不同, Swift在编译时会强制执行许多要求。在Objective-C中放松的事情, 例如id类型和隐式转换, 在Swift中不是事情。即使你有一个Int和Double, 并且想要将它们加起来, 也必须将它们显式转换为单个类型。

另外, 可选是语言的基本组成部分, 即使它们是一个简单的概念, 也需要花费一些时间来适应它们。

在开始时, 你可能想强行打开所有包装, 但这最终会导致崩溃。当你熟悉该语言时, 你会开始喜欢几乎没有运行时错误的方式, 因为在编译时会捕获许多错误。

大多数Swift程序员以前在Objective-C方面都有丰富的经验, 除其他外, 这可能会导致他们使用与其他语言熟悉的相同方式来编写Swift代码。这可能会导致一些严重的错误。

在本文中, 我们概述了Swift开发人员最常见的错误以及避免这些错误的方法。

没错-Objective-C最佳实践不是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, 则强制展开将导致运行时错误并终止应用程序。此外, 尝试访问隐式展开的可选值将导致相同的结果。

有时, 我们无法(或不想)在类/结构初始化器中初始化变量。因此, 我们必须将它们声明为可选。在某些情况下, 我们假定它们在代码的某些部分中不会为零, 因此我们强制对其进行拆包或将它们声明为隐式解包的可选内容, 因为这比始终进行可选绑定要容易。这应该谨慎进行。

这类似于使用IBOutlet, 后者是引用笔尖或情节提要中的对象的变量。它们不会在父对象初始化(通常是视图控制器或自定义UIView)时初始化, 但是我们可以确保在调用viewDidLoad(在视图控制器中)或awakeFromNib(在视图中)时它们不会为零, 这样我们就可以安全地访问它们。

通常, 最佳实践是避免强行打开包装并使用隐式打开包装的可选包装。始终认为可选值可以为nil, 并使用可选绑定适当地对其进行处理, 或者在强制进行拆包之前检查其是否为nil, 或者在隐式打开可选值的情况下访问变量。

2.不知道强参考周期的陷阱

当一对对象彼此保持强引用时, 将存在强引用循环。对于Swift来说, 这并不是什么新鲜事物, 因为Objective-C存在相同的问题, 并且经验丰富的Objective-C开发人员应该适当地管理它。重要的是要注意强引用和引用什么。 Swift文档中有专门针对该主题的部分。

使用闭包时, 管理你的引用尤为重要。默认情况下, 闭包(或块)会强烈引用其中的每个对象。如果这些对象中的任何一个对闭包本身都有很强的引用, 那么我们就有一个强大的引用周期。有必要利用捕获列表来正确管理参考文献的捕获方式。

如果有可能在调用该块之前释放该块捕获的实例, 则必须将其捕获为弱引用, 这是可选的, 因为它可以为nil。现在, 如果你确定捕获的实例在该块的生存期内不会被释放, 则可以将其捕获为无主引用。使用无主而不是弱无用的优点是, 引用不是可选的, 你可以直接使用该值, 而无需将其拆包。

在下面的示例中, 你可以在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永远不会打印, 这意味着我们的实例w不会从内存中释放。要解决此问题, 我们必须使用捕获列表来不强烈捕获自身:

    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

在这种情况下, 我们可以使用unown, 因为self在闭包的生存期内永远不会为零。

最好几乎总是使用捕获列表来避免引用周期, 这将减少内存泄漏, 并最终获得更安全的代码, 这是一个好习惯。

3.到处使用自我

与Objective-C不同, 在Swift中, 我们不需要使用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: +)

该地图将输出一个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)
      }
  }

现在, 代码更加有效, 但是可以使用过滤器对其进行进一步改进:

  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.停留在舒适区, 不要尝试面向协议的编程

正如WWDC的Swift面向协议的编程会话中所提到的, 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 {
          /*___*/
      }
  }

该语句Generator.Element:Shape是约束条件, 它声明扩展中的方法仅在符合CollectionType的类型的实例中可用, 其中CollectionType包含符合Shape的类型的元素。例如, 可以在Array <Shape>的实例上调用这些方法, 但不能在Array <String>的实例上调用。如果我们有一个符合Shape协议的Polygon类, 则这些方法也可用于Array <Polygon>的实例。

使用协议扩展, 你可以为协议中声明的方法提供默认实现, 然后该协议将在符合该协议的所有类型中可用, 而无需对这些类型(类, 结构或枚举)进行任何更改。这在整个Swift标准库中都已广泛完成, 例如, map和reduce在CollectionType的扩展中定义, 并且相同的实现由Array和Dictionary等类型共享, 而无需任何额外的代码。

此行为类似于其他语言(例如Ruby或Python)的mixin。通过简单地使用默认方法实现来遵守协议, 就可以为类型添加功能。

面向协议的程序设计可能看起来很笨拙, 乍一看并没有太大用处, 这可能会让你忽略它, 甚至无法尝试。这篇文章很好地理解了在实际应用中使用面向协议的编程。

据我们了解, Swift不是玩具语言

最初, Swift受到了很多怀疑。人们似乎以为苹果公司将用儿童玩具语言或非程序员语言代替Objective-C。但是, 事实证明, Swift是一种严肃而强大的语言, 它使编程变得非常愉快。由于它是强类型的, 因此很难犯错误, 因此, 很难列出你可以使用该语言犯的错误。

当你习惯了Swift并返回到Objective-C时, 你会注意到其中的区别。你将错过Swift提供的出色功能, 并且必须在Objective-C中编写乏味的代码才能达到相同的效果。有时, 你会遇到Swift在编译过程中会捕获到的运行时错误。对于Apple程序员而言, 这是一次了不起的升级, 随着语言的成熟, 还有很多事情要做。

相关:iOS开发人员指南:从Objective-C到Learn Swift

赞(0)
未经允许不得转载:srcmini » 大多数Swift开发人员不知道自己犯的错误

评论 抢沙发

评论前必须登录!