为什么要创建"Implicitly Unwrapped Optionals",因为这意味着你知道它有价值?

Joh*_*ton 481 design-patterns optional swift

为什么要创建一个"Implicitly Unwrapped Optional"来创建一个常规变量或常量?如果您知道它可以成功解开,那么为什么要首先创建一个可选项呢?例如,为什么这样:

let someString: String! = "this is the string"
Run Code Online (Sandbox Code Playgroud)

会比以下更有用:

let someString: String = "this is the string"
Run Code Online (Sandbox Code Playgroud)

如果"optionals指示常量或变量允许'没有值'",但"有时从程序的结构中可以清楚地看到,在首次设置该值之后,可选项将始终具有值",那么有什么意义呢?首先让它成为可选项?如果你知道一个可选项总是会有一个值,那么它不是可选的吗?

dre*_*wag 456

在我能描述Implicitly Unwrapped Optionals的用例之前,您应该已经了解了Swift中的Optionals和Implicitly Unwrapped Optionals.如果你不这样做,我建议你先阅读我关于期权的文章

何时使用隐式解包可选

创建Implicitly Unwrapped Optional有两个主要原因.所有这些都与定义一个永远不会被访问的变量有关,nil否则,Swift编译器将始终强制您显式展开Optional.

1.初始化期间无法定义的常量

初始化完成时,每个成员常量必须具有值.有时,在初始化期间无法使用正确的值初始化常量,但仍可保证在访问之前具有值.

使用Optional变量解决了这个问题,因为Optional会自动初始化,nil并且它最终包含的值仍然是不可变的.然而,不断展开一个你知道肯定不是零的变量可能会很痛苦.Implicitly Unwrapped Optionals实现与Optional相同的好处,并且具有额外的好处,即不必在任何地方明确地展开它.

一个很好的例子是,在加载视图之前,无法在UIView子类中初始化成员变量:

class MyView: UIView {
    @IBOutlet var button: UIButton!
    var buttonOriginalWidth: CGFloat!

    override func awakeFromNib() {
        self.buttonOriginalWidth = self.button.frame.size.width
    }
}
Run Code Online (Sandbox Code Playgroud)

在这里,您无法在视图加载之前计算按钮的原始宽度,但是您知道awakeFromNib将在视图上的任何其他方法之前调用(初始化除外).您可以将它声明为Implicitly Unwrapped Optional,而不是强制在整个类中无意义地展开值.

2.当你的应用程序无法从变量存在中恢复时 nil

这应该是非常罕见的,但如果您的应用程序无法继续运行,如果nil访问变量,那么打扰测试它将是浪费时间nil.通常情况下,如果您的应用程序继续运行必须绝对正确,那么您将使用assert.一个Implicitly Unwrapped Optional有一个内置于其中的nil断言.即使这样,打开可选项通常也是好的,如果它是零,则使用更具描述性的断言.

何时不使用隐式解包可选

1.懒惰计算的成员变量

有时您有一个永远不应该为nil的成员变量,但在初始化期间无法将其设置为正确的值.一种解决方案是使用Implicitly Unwrapped Optional,但更好的方法是使用一个惰性变量:

class FileSystemItem {
}

class Directory : FileSystemItem {
    lazy var contents : [FileSystemItem] = {
        var loadedContents = [FileSystemItem]()
        // load contents and append to loadedContents
        return loadedContents
    }()
}
Run Code Online (Sandbox Code Playgroud)

现在,成员变量contents在第一次访问时才初始化.这使得类在计算初始值之前有机会进入正确的状态.

注意:这似乎与上面的#1相矛盾.但是,有一个重要的区别.在buttonOriginalWidth上述必须viewDidLoad中期间被设置为防止任何人改变按钮宽度被访问属性之前.

2.其他地方

在大多数情况下,应该避免使用Implicitly Unwrapped Optionals,因为如果误用,整个应用程序在访问时会崩溃nil.如果您不确定变量是否为nil,则始终默认使用普通的Optional.展开一个永远nil不会造成伤害的变量.

  • 这个答案应该针对beta 5进行更新.你不能再使用`if someOptional`. (4认同)
  • @SantaClaus`hasValue`在Optional上定义.我更喜欢`hasValue`的语义和`!= nil`的语义.我觉得对于没有在其他语言中使用过"nil"的新程序员来说,这是更容易理解的.`hasValue`比`nil`更合乎逻辑. (2认同)
  • 看起来`hasValue`是从beta 6中提取出来的.虽然灰烬回来了... https://github.com/AshFurrow/hasValue (2认同)

Cat*_*Man 127

考虑一个对象在构造和配置时可能具有nil属性的情况,但之后是不可变的和非零的(NSImage通常以这种方式处理,尽管在它的情况下它有时仍然有用).隐式解包的选择人员会清理其代码,安全性相对较低(只要保证一个保证,就会安全).

(编辑)尽管如此:常规选项几乎总是可取的.


n8g*_*ray 55

隐式展开的选项对于将属性呈现为非可选属性非常有用,因为它实际上需要在封面下是可选的.这通常是两个相关对象之间"打结"的必要条件,每个对象需要引用另一个.当这两个引用实际上都不是可选的时,它是有意义的,但是在初始化对时,其中一个引用需要为nil.

例如:

// These classes are buddies that never go anywhere without each other
class B {
    var name : String
    weak var myBuddyA : A!
    init(name : String) {
        self.name = name
    }
}

class A {
    var name : String
    var myBuddyB : B
    init(name : String) {
        self.name = name
        myBuddyB = B(name:"\(name)'s buddy B")
        myBuddyB.myBuddyA = self
    }
}

var a = A(name:"Big A")
println(a.myBuddyB.name)   // prints "Big A's buddy B"
Run Code Online (Sandbox Code Playgroud)

任何B实例都应该始终具有有效的myBuddyA引用,因此我们不希望让用户将其视为可选,但我们需要它是可选的,以便我们可以BA引用之前构造它.

然而!这种相互参考要求通常表明紧耦合和设计不良.如果您发现自己依赖于隐式解包的选项,那么您应该考虑重构以消除交叉依赖关系.

  • 为"HOWEVER"警告+1.它可能并非一直都是如此,但它肯定值得注意. (11认同)
  • 我认为他们创建这种语言功能的原因之一是`@ IBOutlet` (7认同)
  • 更清楚为什么这个答案是错误的并且具有危险的误导性:隐式解包的Optionals绝对与内存管理无关,**不会**阻止保留周期.但是,Implicitly Unwrapped Optionals在设置双向引用所描述的情况下仍然有用.因此,只需添加"弱"声明并删除"而不创建强大的保留周期" (6认同)
  • 你仍然在A和B之间有一个强大的参考周期.隐式解包的选项**不要**创建一个弱引用.你仍然需要将myByddyA或myBuddyB声明为弱(可能是myBuddyA) (4认同)

Pal*_*ndo 37

隐式解包的选项是务实的妥协,使混合环境中的工作必须与现有的Cocoa框架及其约定更加愉快,同时还允许逐步迁移到更安全的编程范例 - 没有空指针 - 由Swift编译器强制执行.

Swift book,在The Basics章节中,隐式解包的Optionals部分说:

当在首次定义可选项之后立即确认可选的值存在时,隐式展开的选项很有用,并且可以假定在此后的每个点都存在.Swift中隐式解包选项的主要用途是在类初始化期间,如Unowned References和Implicitly Unwrapped Optional Properties中所述.
...
你可以想到一个隐式解包的可选项,因为它允许在使用它时自动解包.每次使用时,都不要在可选项的名称后面放置感叹号,而是在声明它时在可选项的类型后面添加一个感叹号.

这归结于使用在案件nil-ness通过使用惯例确定性质的,并且不能由编译器类的初始化过程中强制执行.例如,UIViewController从NIB或Storyboard初始化的属性,其中初始化被拆分为单独的阶段,但在viewDidLoad()您可以假设属性通常存在之后.否则,为了满足编译器,您必须使用 强制解包, 可选绑定可选链接, 只是为了模糊代码的主要目的.

Swift书的上面部分也提到了自动引用计数章节:

但是,还有第三种情况,其中两个属性应始终具有值,并且nil一旦初始化完成,这两个属性都不应该是.在这种情况下,将一个类上的无主属性与另一个类上的隐式无包装可选属性组合起来很有用.

这使得一旦初始化完成就可以直接访问这两个属性(没有可选的解包),同时仍然避免了参考周期.

这归结为不是垃圾收集语言的怪癖,因此作为程序员打破保留周期,隐式解包的选项是隐藏这个怪癖的工具.

这涵盖了"何时在代码中使用隐式解包的选项?"的问题.作为应用程序开发人员,您将主要在使用Objective-C编写的库的方法签名中遇到它们,这些库不具有表达可选类型的能力.

使用Swift与Cocoa和Objective-C,使用nil部分:

因为Objective-C不保证对象是非nil的,所以Swift使得参数类型中的所有类和返回类型在导入的Objective-C API中是可选的.在使用Objective-C对象之前,应检查以确保它没有丢失.

在某些情况下,您可能绝对确定Objective-C方法或属性永远不会返回nil对象引用.为了使这个特殊场景中的对象更方便使用,Swift将对象类型导入为隐式解包的选项.隐式展开的可选类型包括可选类型的所有安全功能.此外,您可以直接访问该值,而无需nil自行检查或解包.当您以这种可选类型访问该值而不首先安全地解包它时,隐式解包的可选项检查该值是否缺失.如果缺少该值,则会发生运行时错误.因此,除非您确定不会丢失该值,否则应始终自行检查和解包隐式展开的可选项.

......而且超越这里 小龙


ric*_*ter 19

单行(或几行)简单示例不能很好地涵盖期权的行为 - 是的,如果你声明一个变量并立即为它提供一个值,那么在可选项中没有任何意义.

到目前为止我见过的最好的情况是在对象初始化之后发生的设置,然后使用"保证"遵循该设置,例如在视图控制器中:

class MyViewController: UIViewController {

    var screenSize: CGSize?

    override func viewDidLoad {
        super.viewDidLoad()
        screenSize = view.frame.size
    }

    @IBAction printSize(sender: UIButton) {
        println("Screen size: \(screenSize!)")
    }
}
Run Code Online (Sandbox Code Playgroud)

我们知道printSize将在加载视图后调用它 - 它是一个连接到该视图内部控件的动作方法,我们确保不会以其他方式调用它.所以我们可以省去一些可选的检查/绑定!.Swift无法识别这种保证(至少在Apple解决暂停问题之前),所以你告诉编译器它存在.

但是,这在一定程度上打破了类型安全.如果您的"保证"并非始终存在,那么您有一个隐式解包的可选地点就是您的应用可能崩溃的地方,因此这是一个谨慎使用的功能.此外,一直使用!听起来像你在大喊大叫,没人喜欢.

  • 我认为你误解了OP的问题.OP并没有询问关于期权的一般情况,而是特别关于隐式解包期权的需要/使用(即不是`let foo?= 42`,而是'let foo!= 42`).这没有解决这个问题.(这可能是关于选项的相关答案,请注意,但不是关于隐式展开的选项,它们是不同/相关的动物.) (4认同)
  • 功能语言中可选点的一部分是没有哨兵值.你要么有价值,要么没有价值.您不应该有一个值,表明缺少值. (2认同)

fuj*_*471 15

Apple在Swift编程语言中提供了一个很好的例子- > 自动引用计数 - > 解决类实例之间的强引用循环 - >无主引用和隐式解包可选属性

class Country {
    let name: String
    var capitalCity: City! // Apple finally correct this line until 2.0 Prerelease (let -> var)
    init(name: String, capitalName: String) {
        self.name = name
        self.capitalCity = City(name: capitalName, country: self)
    }
}

class City {
    let name: String
    unowned let country: Country
    init(name: String, country: Country) {
        self.name = name
        self.country = country
    }
}
Run Code Online (Sandbox Code Playgroud)

初始化器City是从初始化器中调用的Country.但是,初始化Country程序无法传递selfCity初始化程序,直到新Country实例完全初始化为止,如两阶段初始化中所述.

要处理此要求,请将capitalCity属性声明Country为隐式展开的可选属性.