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

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

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

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

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

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

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

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

没错-Objective-C最佳实践不是Swift最佳实践。

1.强制展开可选

可选类型的变量(例如String? )可能包含也可能不包含值。 当它们不持有值时,它们等于nil 。 要获取可选值的值,首先必须解开它们的包装 ,这可以通过两种不同的方式进行。

一种方法是使用if letguard 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 ,后者是引用笔尖或情节提要中的对象的变量。 它们不会在父对象初始化(通常是视图控制器或自定义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 { var array: [T] = [] { didSet { arrayDidChange?(array: array) } } var arrayDidChange: ((array: [T]) -> Void)? } class Whatever { var container: Container init() { container = Container() 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 { var array: [T] = [] { didSet { arrayDidChange?(array: array) } } var arrayDidChange: ((array: [T]) -> Void)? } class Whatever { var container: Container init() { container = Container() 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的类型都是引用类型。

接下来,我们有使用structenum声明的值类型。 将值类型分配给变量或作为参数传递给函数或方法时,将复制它们。 如果您在复制的实例中进行了更改,则原始实例将不会被修改。 值类型是不可变的 。 如果将新值分配给值类型的实例的属性(例如CGPointCGSize ,则会使用更改创建一个新实例。 这就是为什么我们可以在数组上使用属性观察器的原因(如上面Container类中的示例所示)来通知我们更改。 实际发生的情况是,使用更改创建了一个新数组; 将其分配给属性,然后调用didSet

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

5.不使用枚举的全部潜力

当谈到枚举时,我们通常会想到基本的C枚举,它只是相关常量的列表,这些常量是下面的整数。 在Swift中,枚举的功能更加强大。 例如,您可以将值附加到每个枚举案例。 枚举还具有方法和只读/计算属性,可用于用各种信息和详细信息丰富每种情况。

枚举的官方文档非常直观,并且错误处理文档介绍了一些用例,以说明枚举在Swift中的强大功能。 另外,请在对Swift中的枚举进行广泛探索之后,查看一下您可以使用它们进行的几乎所有操作。

6.不使用功能部件

Swift标准库提供了许多函数式编程基础的方法,这些方法使我们仅用一行代码即可完成很多工作,例如map,reduce和filter等。

我们来看几个例子。

假设您必须计算表格视图的高度。 给定您有一个UITableViewCell子类,如下所示:

 class CustomCell: UITableViewCell { func configureWithModel(model: Model) 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() { 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.保持舒适地带,不要尝试面向协议的编程

正如WWDC的Swift面向协议的编程会话中所提到的,Swift被称为是第一种面向协议的编程语言 。 基本上,这意味着我们可以围绕协议对程序进行建模,并只需通过遵循协议并对其进行扩展就可以为类型添加行为。 例如,给定我们具有Shape协议,我们可以扩展CollectionType (它与ArraySetDictionary等类型Array ),并向其添加一种方法来计算相交的总面积

 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的类型的实例中可用,其中CollectionType包含符合Shape的类型的元素。 例如,可以在Array的实例上调用这些方法,但不能在Array的实例上调用。 如果我们有一个符合Shape协议的Polygon类,则这些方法也可用于Array的实例。

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

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

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

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

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

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

通过Toptal的文章