原文:Swift: Banning force unwrapping optionals

在这篇文章中,我将评论强制解包的危害,以及怎么防止强制解包。

Swift 可选类型和强制解包

Swift 编程语言支持可选类型(optional types)用于处理没有值的状况。一个可选类型代表两种或许性:要么有值,你能够解包这个可选类型来拜访这个值,要么根本就没有值。

以下是怎么在 Swift 中声明一个可选类型变量的方式:

var myOptionalString: String?

myOptionalString 变量能够包括一个 String 类型的字符串值或 nil。假如该可选类型有值,你能够经过运用感叹号操作符显式地解包它来拜访底层存储的值,比方:

print("myOptionalString 变量的值是:(myOptionalString!).")

强制解包的危害

虽然强制解包看起来很方便,但运用起来却十分风险:假如可选类型没有值,而你试图解包它,就会触发运转时过错——通常会导致应用程序溃散。

我在 Swift 代码中看到的绝大多数应用程序的溃散都是由一个不正确的显式解包引起的。为什么有人要解开一个值为 nil 的可选类型呢?有很多原因:

  • 一个可选类型变量的值从来都不是 nil,只不过在重构后会变成 nil
  • 代码仿制和张贴;
  • 可选类型变量很少或许为零,但产生了一些边缘状况;

照我说的做,而不是照我做的

Swift 文档

Swift 文档在一个注释中提到,你应该谨慎运用强制解包的办法:

Swift:制止运用强制解包可选类型


WWDC session 407:不要乱用强制解包

相同,WWDC 2017 session 407 “了解未定义行为” 在幻灯片125中告知我们:“不要乱用强制解包”。


其他 WWDC session 中乱用强行拆包的状况

观看 WWDC 2017 的任何其他 session,你会看到很多(太多了)强制解包。例如,看看 session 203 “介绍拖放”。在120号幻灯片上,你能够看到:

let dragView = interaction.view!

这行代码真的安全吗?在文档中没有任何地方能够看到 UIDragInteraction 的视图被保证有一个值。别的,假如它被保证有一个值,为什么它在 API 中是可选类型呢?


Apple 乱用强制解包的示例代码

Apple 开发者网站随机下载一个示例代码,看看是否以及怎么运用强制解包。在这篇文章中,我下载了最新的 Swift 示例代码。运用 Photos 框架的示例应用程序。在搜索强制解包操作符时,你能够找到这样的代码:

Swift:制止运用强制解包可选类型

这段代码假设 textFieldsalertController 的数组至少包括一个对象。为什么不运用更安全的东西,比方:

alertController.addAction(UIAlertAction(title: NSLocalizedString("Create", comment: ""), style: .default) { _ in
         if let title = alertController.textFields?.first?.text, !title.isEmpty {

同一项目中的另一个比如:

Swift:制止运用强制解包可选类型

写成这样不是更简练、更安全吗?

@IBAction func play(_ sender: AnyObject) {
    if let player = playerLayer.player {
        // An AVPlayerLayer has already been created for this asset; just play it.
        player.play()
} else {

Xcode的 Fix-it 功能建议刺进强制解包操作符…即便它没有意义:

Swift:制止运用强制解包可选类型

怎么防止强制解包

有多种解决方案,比运用强制解包更简练、更安全。我还发现这些代码通常更易读。下面是几个比如:


可选绑定(if let

下面的代码是正确的,但运用了 2 个强制解包:

// 先判别数组中的第一个对象不为空,再强制解包获取这个对象并判别它的类型
if myArray.first?.object != nil && myArray.first!.object!.type == .type1 {
    // Do Something
}

经过运用可选绑定的方式,这段代码的可读性大大增强:

if let object = myArray.first?.object, object.type == .type1 {
    // Do Something
}

另一个类似的或许会导致应用程序溃散的比如:

// 设置 myView 的高度 = myLabel 的高度
// 假如 myLabel 当时还没有被制作到屏幕上,就会导致应用程序运转溃散
myView.setHeight((myLabel?.frame.size.height)!)

相同,一个 if let 句子能够防止或许呈现的溃散。请注意运用相同的变量名来拜访解包后的值这一巧妙的技巧:

if let textLabel = textLabel {
    badgeView.setHeight(textLabel.frame.size.height)
}

在一个 if 句子中的 Guard 句子和多个可选绑定

看看下面这个函数。所有的过错处理都是重复的代码。还要注意 as! 操作符的运用,假如 theObject.value 的值不是 NSNumber 类型,就会导致应用程序运转溃散:

func( object inObject: ObjectType ) {
    if inObject.child!.childType == .childType1 {
        if let theObject = inObject.child!.object(forType: .childType2) {
            if inObject != theObject {
                if let theValue = theObject.value as! NSNumber?, theValue != true {
                    // Do something
                }
                else {
                    // Error handling
                }
            }
            else {
                // Error handling
            }
        }
        else {
            // Error handling
        }
    }
    else {
        // Error handling
    }
}

相反,运用提前退出,将 if 句子分组并运用 as?

func( object inObject: ObjectType ) {
    // guard 句子判别条件
    guard let child = inObject.child  else {
        return
    }
    // 在单个 if 句子中一起判别多个条件
    if child.childType == .childType1,
        let theObject = child.object(forType: .childType2), inObject != theObject,
        let theValue = theObject.value as? NSNumber, theValue != true {
            // Do something
    } else {
        // Error handling
    }

空合运算符(??

尽管以下代码是安全的,但它并不优雅:

if newState != nil {
    self.state = newState!
}
else {
    self.state = .default
}

运用空合运算符要干净得多:

self.state = newState ?? .default

用于初始化成员变量的闭包表达式

鄙人面的比如中,假如 myFunc2()myFunc1() 之前被调用,你或许会得到一个溃散:

var myObject: MyObject?
func myFunc1() {
    self.myObject = MyObject.init()
}
func myFunc2() {
    self.myObject!.rotate()
}

你能够运用一个闭包表达式来初始化变量:

var myObject: MyObject = {
    return MyObject.init()
}()
func myFunc1() {
}
func myFunc2() {
    self.myObject.rotate()
}

隐式解包可选类型

隐式解包可选类型的状况与强制解包十分相似。来自 Swift 文档:

An implicitly unwrapped optional is a normal optional behind the scenes, but can also be used like a nonoptional value, without the need to unwrap the optional value each time it is accessed.

一个隐式解包的可选类型值在背面是一个普通的可选类型值,但也能够像非可选类型值相同运用,而不需要在每次拜访时解包可选类型值。

类似于强制解包可选类型,我看到很多代码风险地运用隐式解包可选类型。这种选项也应该被防止……除了在一种状况下它是十分有用的。当声明一个 IBOutlet 类型时,你应该运用一个隐式解包可选类型,由于一旦 viewDidLoad 办法被调用,IBOutlet 将被初始化。别的在 viewDidLoad 办法之前运用 IBOutlet 将是一个编程过错。

例如,你能够这样写:

@IBOutlet weak var myLabel: UILabel!

总结

Apple 告知我们要遵守运用强制解包的规则。但是,在 WWDC 的幻灯片以及示例代码中,常常运用强制解包操作。这让人很伤心,由于 Apple 重复运用强制解包会让人养成坏习惯,你会很容易忘记强制解包的风险。

在代码中制止运用强制解包,能够使你的代码更安全,防止一堆溃散。此外,你的代码将愈加可读和简练。