为什么要创建“隐式展开可选”而不是仅创建常规变量或常量?如果您知道它可以成功解包,那么为什么要首先创建一个可选项?例如,为什么会这样:
let someString: String! = "this is the string"
将比以下更有用:
let someString: String = "this is the string"
如果“可选值表示允许一个常量或变量‘无值’”,但“有时从程序的结构中可以清楚地看出,在第一次设置该值之后,可选值总是有一个值”,那么重点是什么?首先让它成为一个可选的?如果你知道一个可选项总是有一个值,那不是让它不是可选的吗?
在我描述 Implicitly Unwrapped Optionals 的用例之前,您应该已经了解什么是 Swift 中的 Optionals 和 Implicitly Unwrapped Optionals。如果您不这样做,我建议您先阅读my article on optionals
何时使用隐式展开的 Optional
有两个主要原因会创建一个隐式展开的 Optional。所有这些都与定义一个在 nil
时永远不会被访问的变量有关,否则,Swift 编译器将总是强制你显式地打开一个 Optional。
1. 初始化时无法定义的常量
在初始化完成时,每个成员常量都必须有一个值。有时,一个常量在初始化时不能用正确的值初始化,但在被访问之前仍然可以保证它有一个值。
使用 Optional 变量可以解决这个问题,因为 Optional 使用 nil
自动初始化,并且它最终包含的值仍然是不可变的。然而,不断解开一个你确定不是 nil 的变量可能会很痛苦。 Implicitly Unwrapped 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
。 Implicitly Unwrapped Optional 有一个内置的 nil 断言。即便如此,如果它是 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
}()
}
现在,成员变量 contents
直到第一次被访问时才被初始化。这使类有机会在计算初始值之前进入正确的状态。
注意:这似乎与上面的#1 相矛盾。但是,有一个重要的区别。上面的 buttonOriginalWidth
必须在 viewDidLoad 期间设置,以防止任何人在访问属性之前更改按钮宽度。
2. 其他地方
在大多数情况下,应避免使用隐式展开的 Optionals,因为如果使用不当,您的整个应用程序将在 nil
时访问它时崩溃。如果您不确定变量是否可以为 nil,请始终默认使用普通的 Optional。解开一个永远不是 nil
的变量当然不会有太大的伤害。
考虑一个对象在构造和配置时可能具有 nil 属性的情况,但之后是不可变且非 nil 的(NSImage 通常以这种方式处理,尽管在这种情况下,有时变异仍然有用)。隐式展开的可选项会很好地清理其代码,安全性损失相对较低(只要有一个保证,它就是安全的)。
(编辑)但要明确一点:常规选项几乎总是更可取的。
隐式展开的可选项对于将属性显示为非可选非常有用,因为它确实需要在幕后是可选的。这对于在两个相关对象之间“打结”通常是必要的,每个对象都需要引用另一个对象。当两个引用实际上都不是可选的时,这是有道理的,但是在初始化对时,其中一个需要为 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"
任何 B
实例都应始终具有有效的 myBuddyA
引用,因此我们不想让用户将其视为可选,但我们需要它是可选的,以便我们可以在获得 B
之前构造一个A
参考。
然而!这种相互参考要求通常表明紧密耦合和糟糕的设计。如果您发现自己依赖于隐式展开的选项,您可能应该考虑重构以消除交叉依赖。
@IBOutlet
weak
声明并删除“不创建强保留周期”
隐式展开的可选项是一种实用的折衷方案,可以使混合环境中的工作变得更加愉快,该环境必须与现有的 Cocoa 框架及其约定进行互操作,同时还允许逐步迁移到由 Swift 编译器强制执行的更安全的编程范式(没有空指针)。
Swift book, in The Basics chapter, section Implicitly Unwrapped Optionals 说:
当一个可选项的值在第一次定义后立即确认存在并且可以肯定地假定在此后的每个点都存在时,隐式展开的可选项很有用。 Swift 中隐式解包选项的主要用途是在类初始化期间,如无主引用和隐式解包选项属性中所述。 ...您可以将隐式解包的可选项视为允许在使用时自动解包该选项。不是每次使用时在可选项名称后放置一个感叹号,而是在声明它时在可选项的类型后放置一个感叹号。
这归结为属性的非nil
属性是通过使用约定建立的,并且在类初始化期间不能由编译器强制执行的用例。例如,从 NIB 或 Storyboard 初始化的 UIViewController
属性,其中初始化被拆分为单独的阶段,但在 viewDidLoad()
之后,您可以假设属性通常存在。否则,为了满足编译器的要求,您只能使用 forced unwrapping、optional binding 或 optional chaining 来掩盖代码的主要目的。
Swift 书中的上述部分也提到了 Automatic Reference Counting chapter:
但是,还有第三种情况,其中两个属性都应该始终有一个值,并且一旦初始化完成,两个属性都不应该为 nil。在这种情况下,将一个类上的无主属性与另一个类上的隐式展开的可选属性结合起来很有用。这使得一旦初始化完成就可以直接访问这两个属性(无需可选的展开),同时仍然避免引用循环。
这归结为不是垃圾收集语言的怪癖,因此保留周期的中断取决于您作为程序员,隐式展开的选项是隐藏此怪癖的工具。
这涵盖了“何时在代码中使用隐式展开的选项?”问题。作为应用程序开发人员,您将主要在用 Objective-C 编写的库的方法签名中遇到它们,它没有表达可选类型的能力。
从 Using Swift with Cocoa and Objective-C, section Working with nil:
因为 Objective-C 不保证一个对象是非 nil 的,所以 Swift 使所有类的参数类型和返回类型在导入的 Objective-C API 中都是可选的。在使用 Objective-C 对象之前,您应该检查以确保它没有丢失。在某些情况下,您可能绝对确定某个 Objective-C 方法或属性永远不会返回 nil 对象引用。为了使这种特殊场景中的对象更易于使用,Swift 将对象类型导入为隐式展开的可选项。隐式展开的可选类型包括可选类型的所有安全特性。此外,您可以直接访问该值,而无需检查 nil 或自行解包。当您访问这种可选类型中的值而没有先安全地解包它时,隐式解包的可选项会检查该值是否丢失。如果缺少该值,则会发生运行时错误。因此,您应该始终自己检查并解包隐式解包的可选项,除非您确定该值不会丢失。
https://i.stack.imgur.com/mdCqJ.png
单行(或多行)简单示例并不能很好地涵盖可选项的行为——是的,如果您声明一个变量并立即为其提供值,那么可选项就没有任何意义。
到目前为止我见过的最好的情况是在对象初始化之后发生的设置,然后使用“保证”遵循该设置,例如在视图控制器中:
class MyViewController: UIViewController {
var screenSize: CGSize?
override func viewDidLoad {
super.viewDidLoad()
screenSize = view.frame.size
}
@IBAction printSize(sender: UIButton) {
println("Screen size: \(screenSize!)")
}
}
我们知道 printSize
将在视图加载后被调用——它是一个连接到该视图内的控件的操作方法,我们确保不调用它。所以我们可以用 !
为自己节省一些可选的检查/绑定。 Swift 无法识别这种保证(至少在 Apple 解决停机问题之前),所以你告诉编译器它存在。
不过,这在某种程度上破坏了类型安全。如果您的“保证”并不总是成立,那么您拥有隐式展开的可选选项的任何地方都是您的应用程序可能崩溃的地方,因此这是一个谨慎使用的功能。此外,一直使用 !
会让您听起来像是在大喊大叫,而没有人喜欢这样。
CGSizeZero
在实际使用中可能是一个很好的标记值。但是,如果您从 nib 加载的尺寸实际上可能为零怎么办?然后使用 CGSizeZero
作为标记并不能帮助您区分未设置的值和设置为零的值。此外,这同样适用于从 nib(或 init
之后的任何其他位置)加载的其他类型:字符串、对子视图的引用等。
let foo? = 42
,而是 let foo! = 42
)的需要/使用。这并没有解决这个问题。 (请注意,这可能是关于选项的相关答案,但不是关于隐式展开的选项,它们是不同/相关的动物。)
Apple 在 The Swift Programming Language 中给出了一个很好的例子 -> Automatic Reference Counting -> 解决类实例之间的强引用循环 -> 无主引用和隐式展开的可选属性
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
}
}
从 Country 的初始化程序中调用 City 的初始化程序。但是,在完全初始化新的 Country 实例之前,Country 的初始化程序无法将 self 传递给 City 初始化程序,如两阶段初始化中所述。为了满足这一要求,您将 Country 的 capitalCity 属性声明为隐式展开的可选属性。
通过首先查看强制展开的基本原理,更容易解释隐式选项的基本原理。
使用 !运算符,意味着您确定您的代码没有错误,并且可选项已经有一个正在解包的值。没有!运算符,您可能只需使用可选绑定进行断言:
if let value = optionalWhichTotallyHasAValue {
println("\(value)")
} else {
assert(false)
}
这不如
println("\(value!)")
现在,隐式可选项让您可以在所有可能的流程中表达您希望在展开时始终具有值的可选项。所以它只是在帮助你方面更进一步——通过放宽编写!每次都打开包装,并确保运行时在您对流程的假设错误的情况下仍然会出错。
init
中设置(你甚至可能没有实现),但它们保证设置在 awakeFromNib
/ viewDidLoad
之后。
如果您确定从可选项而不是 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
Implicitly Unwrapped Optional
(IUO
)
它是 Optional
的语法糖,不会强制程序员解包变量。它可用于在 two-phase initialization process
期间无法初始化且 隐含 非零的变量。此变量的行为类似于 non-nil,但实际上是一个 可选 变量。一个很好的例子是 - Interface Builder's outlets
Optional
通常更可取
var implicitlyUnwrappedOptional: String! //<- Implicitly Unwrapped Optional
var nonNil: String = ""
var optional: 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
}
我认为 Optional
是一个让很多初学者感到困惑的结构的坏名字。
其他语言(例如 Kotlin 和 C#)使用术语 Nullable
,这使得它更容易理解。
Nullable
表示您可以将空值分配给此类型的变量。所以如果它是 Nullable<SomeClassType>
,你可以给它分配空值,如果它只是 SomeClassType
,你不能。这就是 Swift 的工作方式。
为什么要使用它们?好吧,有时你需要有空值,这就是原因。例如,当您知道要在一个类中拥有一个字段,但在创建该类的实例时不能将其分配给任何东西,但稍后您会这样做。我不会给出例子,因为人们已经在这里提供了它们。我写这个只是为了给我的2美分。
顺便说一句,我建议你看看这在其他语言中是如何工作的,比如 Kotlin 和 C#。
以下是在 Kotlin 中解释此功能的链接:https://kotlinlang.org/docs/reference/null-safety.html
其他语言(如 Java 和 Scala)确实有 Optional
,但它们的工作方式与 Swift 中的 Optional
不同,因为默认情况下 Java 和 Scala 的类型都可以为空。
总而言之,我认为这个特性在 Swift 中应该被命名为 Nullable
,而不是 Optional
...
if someOptional
。hasValue
在 Optional 上定义。我更喜欢hasValue
的语义而不是!= nil
的语义。我觉得对于没有用过其他语言的nil
的新程序员来说更容易理解。hasValue
比nil
更符合逻辑。hasValue
是从 beta 6 中删除的。Ash 把它放回去了……github.com/AshFurrow/hasValueNote: This may seem to contradict #1 from above. However, there is an important distinction to be made. The buttonOriginalWidth above must be set during viewDidLoad to prevent anyone changing the buttons width before the property is accessed.