为什么要创建“隐式展开的可选项”,因为这意味着您知道存在价值?


493

为什么只创建一个正则变量或常量而不是创建一个“隐式展开的可选”?如果您知道可以成功解包,那么为什么首先要创建一个可选的?例如,为什么这样:

let someString: String! = "this is the string"

比以下内容更有用:

let someString: String = "this is the string"

如果“可选参数指示允许常量或变量具有'无值'”,但“有时从程序的结构中可以看出,可选参数在首次设置该值后将始终具有一个值”,这是什么意思呢?首先使它成为可选项?如果您知道可选值总是会有一个值,那不是使它不是可选值吗?

Answers:


127

考虑一个对象的情况,该对象在构造和配置时可能具有nil属性,但是此后是不可变的且非nil(NSImage通常以这种方式处理,尽管在这种情况下,有时进行突变仍然很有用)。隐式解开的可选对象将清理其代码,并且安全损失相对较低(只要持有一个保证,它将是安全的)。

(编辑)但是要明确:常规的可选几乎总是首选。


459

在描述隐式展开的可选的用例之前,您应该已经了解Swift中的Optional和隐式展开的可选。如果您不这样做,建议您先阅读我的关于可选产品的文章

何时使用隐式展开的可选

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

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

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

使用Optional变量可以解决此问题,因为Optional将自动初始化,nil并且它最终将包含的值仍然是不可变的。但是,不断地展开一个肯定不会为零的变量可能会很痛苦。隐式展开的Optional具有与Optional相同的优点,并且具有不必在任何地方显式展开它的附加优点。

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

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

    override func awakeFromNib() {
        self.buttonOriginalWidth = self.button.frame.size.width
    }
}

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

2.当您的应用无法从变量中恢复时 nil

这应该非常少见,但是如果nil在访问变量时您的应用无法继续运行,则浪费时间进行测试nil。通常,如果您的条件必须完全正确才能使应用程序继续运行,则可以使用assert。隐式展开的Optional内置了一个nil的断言。即使这样,通常也可以拆开可选选项,并使用更具描述性的断言(如果为零)。

何时不使用隐式展开的可选

1.延迟计算的成员变量

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

class FileSystemItem {
}

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

现在,成员变量contents直到第一次被访问才被初始化。这使班级有机会在计算初始值之前进入正确的状态。

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

2.其他地方

在大多数情况下,应避免使用“隐式解包的Optionals”,因为如果使用不当,当您访问时,您的整个应用程序将崩溃nil。如果您不确定某个变量是否可以为nil,请始终默认使用常规的Optional。解开一个永远nil不会带来不确定性的变量不会带来太大的伤害。


4
此答案应针对beta 5进行更新。您不能再使用if someOptional
圣诞老人2014年

2
@SantaClaus hasValue是在Optional上定义的。我喜欢的语义hasValue,以那些!= nil。对于没有使用nil其他语言的新程序员,我觉得这更容易理解。hasValuenil。更具逻辑性。
drewag

2
看起来像是hasValue从beta 6中撤出的。Ash
Chris Wagner

1
@newacct关于Objc初始值设定项的返回类型,它更多是隐式的Implicitly Unwrapped Optional。您描述的使用“非可选”行为正是隐式展开的可选的行为(直到被访问才失败)。关于通过强制展开使程序更早失败,我同意首选使用非可选方法,但这并不总是可行的。
drewag

1
@confile号 无论如何,它将作为一个指针出现在Objective-C中(如果它是可选的,隐式解包的或非可选的)。
drewag

56

隐式解开的可选选项对于在属性实际上需要在幕后为可选属性时将其表示为非可选属性很有用。这对于在每个需要相互引用的两个相关对象之间“打结”通常是必需的。当两个引用实际上都不是可选的,但是在初始化该对时,其中一个需要为零时,这是有意义的。

例如:

// 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"

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

然而!这种相互参考的要求通常表示紧密耦合和不良设计。如果您发现自己依赖于隐式解包的可选组件,则可能应该考虑进行重构以消除交叉依赖关系。


7
我认为他们创建此语言功能的原因之一是@IBOutlet
Jiaaro 2014年

11
为“ HOWEVER”告诫+1。可能并非一直如此,但肯定是需要注意的事情。
JMD 2014年

4
您在A和B之间仍然有很强的参考周期。隐式展开的可选对象不要创建弱参考。您仍然需要将myByddyA或myBuddyB声明为弱(可能是myBuddyA)
drewag

6
为了更清楚地说明为什么这个答案是错误的并且可能引起误导:隐式解包Optional与内存管理完全无关,也不阻止保留周期。但是,在为设置双向引用而描述的情况下,隐式解开的Optionals仍然有用。因此,只需添加weak声明并删除“无需创建强大的保留周期”即可
drewag

1
@drewag:您是对的-我已经编辑了答案以删除保留周期。我打算使反向引用变弱,但我想它使我无所适从。
n8gray 2014年

37

隐式展开的可选选项是实用的折衷方案,使在必须与现有Cocoa框架及其约定互操作的混合环境中的工作更加愉快,同时还允许逐步移植到Swift编译器强制执行的更安全的编程范例(无空指针)中。

一本Swift书,在“基础知识”一章的“ 隐式解开可选内容部分中说:

当在第一个定义了可选对象的值之后立即确认该可选对象的值存在并且可以肯定地假定此后的每个点都存在时,隐式解开的可选对象很有用。Swift中隐式解包的可选对象的主要用途是在类初始化期间,如无主引用和隐式解开的可选属性中所述

您可以将隐式解压缩的可选内容视为允许使用该可选内容以使其自动解包的权限。不必在每次使用可选名称时都在其名称后放置一个感叹号,而是在声明它时在其可选类型后放置一个感叹号。

这归结于使用在案件nil-ness通过使用惯例确定性质的,并且不能由编译器类的初始化过程中强制执行。例如,UIViewController从NIB或情节提要板初始化的属性,其中将初始化分为几个阶段,但是之后viewDidLoad()您可以假定属性通常存在。否则,为了使编译器满意,您必须仅使用 强制解包可选绑定可选链接 来掩盖代码的主要用途。

Swift书的上方部分还涉及“ 自动引用计数”一章

但是,存在第三种情况,在这两种情况下,两个属性都应始终具有值,并且nil初始化完成后,这两个属性都不应具有。在这种情况下,将一个类上的未拥有属性与另一类上的隐式展开的可选属性结合起来很有用。

这样,初始化完成后就可以直接访问这两个属性(没有可选的展开),同时仍然避免了引用周期。

这归结为不是垃圾收集语言的怪癖,因此,作为程序员,保留周期的中断就在您身旁,而隐式展开的可选变量则是隐藏此怪癖的工具。

涵盖了“何时在代码中使用隐式解开的可选内容?” 题。作为应用程序开发人员,您经常会在用Objective-C编写的库的方法签名中遇到这些签名,而Objective-C无法表达可选类型。

从结合使用Swift和Cocoa和Objective-C中,使用nil部分

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

在某些情况下,您可以绝对确定Objective-C方法或属性从不返回nil对象引用。为了使在这种特殊情况下的对象更易于使用,Swift将对象类型作为隐式解包的optional导入。隐式展开的可选类型包括可选类型的所有安全功能。此外,您无需检查是否可以直接访问该值nil会将或自己解。当您在不首先安全地对其进行拆包的情况下访问这种可选类型的值时,隐式解包的可选检查该值是否丢失。如果缺少该值,则会发生运行时错误。因此,除非您确定不会丢失该值,否则应始终自己检查并解开一个隐式解包的可选文件。

...然后躺在这里 龙


感谢您提供详尽的答案。您能想到一个快速清单,该清单何时使用隐式解包的可选内容,何时使用标准变量就足够了?
Hairgami_Master 2014年

@Hairgami_Master我在列表和具体示例中添加了自己的答案
drewag

18

单行(或多行)的简单示例不能很好地说明可选变量的行为-是的,如果您声明一个变量并立即为其提供值,那么可选变量毫无意义。

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

class MyViewController: UIViewController {

    var screenSize: CGSize?

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

    @IBAction printSize(sender: UIButton) {
        println("Screen size: \(screenSize!)")
    }
}

我们知道printSize在视图加载后将被调用—这是一个与该视图内的控件关联的action方法,我们确保不要以其他方式调用它。因此,我们可以使用来保存一些可选的检查/绑定!。Swift无法识别这种保证(至少直到苹果公司解决了暂停问题),所以您告诉编译器它已经存在。

但是,这在某种程度上破坏了类型安全性。如果您的“担保”并不总是成立,则在任何具有隐式解包的可选内容的地方,应用程序都可能崩溃,因此该功能可少量使用。此外,一直使用!时间听起来好像在大吼大叫,没人喜欢。


为什么不将screenSize初始化为CGSize(height:0,width:0)并省去每次访问该变量都必须大喊大叫的麻烦呢?
Martin Gordon

大小可能不是最好的例子,因为CGSizeZero在实际使用中,大小可能是一个很好的标记值。但是,如果从笔尖加载的大小实际上可能为零,该怎么办?然后,CGSizeZero用作哨兵并不能帮助您区分未设置的值和设置为零的值。此外,这同样适用很好地(后或其他地方从笔尖加载其他类型init):字符串,以子视图的引用,等等
rickster

2
功能语言中Optional的部分要点是没有哨兵值。您要么拥有价值,要么就没有价值。您不应有一个值指示缺少值的情况。
韦恩·坦纳

4
我认为您误解了OP的问题。OP并不是在问可选对象的一般情况,而是在询问是否需要/使用隐式展开的可选对象(即不是let foo? = 42,而是let foo! = 42)。这没有解决。(请注意,这可能是有关可选内容的相关答案,但与涉及不同/相关动物的隐式展开的可选内容无关。)
JMD 2014年

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
    }
}

从的初始化器City中调用的初始化器Country。但是,如两阶段初始化中所述,在将新实例完全初始化之前,的初始化Country程序无法传递self初始化程序CityCountry

为了满足此要求,您可以将的capitalCity属性声明Country为隐式展开的可选属性。


这个答案中提到的教程在这里
富兰克林于

3

隐式可选的基本原理更容易通过首先查看强制展开的基本原理来解释。

使用!强制展开可选(隐式或非隐式)的包装 运算符,意味着您可以确定您的代码没有错误,并且可选代码在解包时已经具有一个值。没有!运算符,您可能只需声明一个可选绑定即可:

 if let value = optionalWhichTotallyHasAValue {
     println("\(value)")
 } else {
     assert(false)
 }

不如

println("\(value!)")

现在,隐式可选选项使您可以表示具有一个可选选项,该选项在展开时在所有可能的流中都希望始终具有一个值。因此,通过放宽编写!的要求,它为您提供了进一步的帮助!每次解包,并确保在您对流的假设错误的情况下,运行时仍会出错。


1
@newacct:通过代码的所有可能流中的非零与通过释放分配从父级(类/结构)初始化的非零不同。Interface Builder是经典的示例(但是还有许多其他延迟的初始化模式):如果仅从笔尖使用一个类,则不会在init其中设置出口变量(您甚至可能不会实现),但是它们重新保证后,设置awakeFromNib/ viewDidLoad
rickster

3

如果您确定知道的话,值是从可选值返回的,而不是nil隐式展开的可选值用于直接从可选值中捕获那些值,而非可选值则不能。

//Optional string with a value
let optionalString: String? = "This is an optional String"

//Declaration of an Implicitly Unwrapped Optional String
let implicitlyUnwrappedOptionalString: String!

//Declaration of a non Optional String
let nonOptionalString: String

//Here you can catch the value of an optional
implicitlyUnwrappedOptionalString = optionalString

//Here you can't catch the value of an optional and this will cause an error
nonOptionalString = optionalString

所以这是使用之间的区别

let someString : String! let someString : String


1
这不能回答OP的问题。OP 知道什么是隐式展开可选。
富兰克林·于

0

我认为 Optional对于这种结构,是一个不好的名字,它使很多初学者感到困惑。

其他语言(例如Kotlin和C#)使用该术语 Nullable,这使它的理解变得更加容易。

Nullable表示您可以为该类型的变量分配空值。因此,如果是Nullable<SomeClassType>,则可以为其分配空值(如果只是)SomeClassType,则不能。这就是Swift的工作方式。

为什么要使用它们?好吧,有时候您需要使用null,这就是为什么。例如,当您知道要在一个类中具有一个字段,但是在创建该类的实例时不能将其分配给任何对象,但是稍后再说。我不会给出示例,因为人们已经在此处提供了示例。我只是写这个给我2美分。

顺便说一句,我建议您看看这在其他语言(例如Kotlin和C#)中如何工作。

这是解释Kotlin中此功能的链接: https //kotlinlang.org/docs/reference/null-safety.html

其他语言(例如Java和Scala)也有Optional,但它们的工作方式与Optional Swift中的s的,因为Java和Scala的类型默认情况下都是可为空的。

总而言之,我认为该功能应该Nullable在Swift中命名,而不是Optional...


0

Implicitly Unwrapped Optional是一种语法糖,Optional因为它不会迫使程序员拆开变量。它可以用于在初始化期间不能初始化的变量,two-phase initialization process并且表示非零。此变量本身表现为非nil,但实际上是可选变量。一个很好的例子是-Interface Builder的插座

Optional 通常比较可取

var nonNil: String = ""
var optional: String?
var implicitlyUnwrappedOptional: String!

func foo() {
    //get a value
    nonNil.count
    optional?.count

    //Danderour - makes a force unwrapping which can throw a runtime error
    implicitlyUnwrappedOptional.count

    //assign to nil
//        nonNil = nil //Compile error - 'nil' cannot be assigned to type 'String'
    optional = nil
    implicitlyUnwrappedOptional = nil
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.