ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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

将比以下更有用:

let someString: String = "this is the string"

如果“可选值表示允许一个常量或变量‘无值’”,但“有时从程序的结构中可以清楚地看出,在第一次设置该值之后,可选值总是有一个值”,那么重点是什么?首先让它成为一个可选的?如果你知道一个可选项总是有一个值,那不是让它不是可选的吗?


d
drewag

在我描述 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 的变量当然不会有太大的伤害。


此答案应针对 beta 5 进行更新。您不能再使用 if someOptional
@SantaClaus hasValue 在 Optional 上定义。我更喜欢 hasValue 的语义而不是 != nil 的语义。我觉得对于没有用过其他语言的nil的新程序员来说更容易理解。 hasValuenil 更符合逻辑。
看起来 hasValue 是从 beta 6 中删除的。Ash 把它放回去了……github.com/AshFurrow/hasValue
@newacct 关于 Objc 初始化程序的返回类型,它更像是一个隐式的 Implicitly Unwrapped Optional。您描述的使用“非可选”的行为正是隐式展开的可选会执行的操作(在被访问之前不会失败)。关于通过强制解包让程序提前失败,我同意首选使用非可选,但这并不总是可能的。
我还是没看懂下面这句话。任何人都可以详细说明吗? Note: 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.
C
Catfish_Man

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

(编辑)但要明确一点:常规选项几乎总是更可取的。


n
n8gray

隐式展开的可选项对于将属性显示为非可选非常有用,因为它确实需要在幕后是可选的。这对于在两个相关对象之间“打结”通常是必要的,每个对象都需要引用另一个对象。当两个引用实际上都不是可选的时,这是有道理的,但是在初始化对时,其中一个需要为 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
+1 表示“但是”警告。这可能并不总是正确的,但它肯定是需要注意的。
您在 A 和 B 之间仍然有一个强引用循环。隐式展开的选项不会创建弱引用。您仍然需要将 myByddyA 或 myBuddyB 声明为弱(可能是 myBuddyA)
更清楚为什么这个答案是错误的并且具有危险的误导性:隐式展开的选项与内存管理绝对无关,并且阻止保留周期。然而,隐式展开的选项在设置双向引用的情况下仍然有用。因此,只需添加 weak 声明并删除“不创建强保留周期”
@drewag:你说得对——我已经编辑了答案以删除保留周期。我打算使反向引用变弱,但我想它溜走了。
P
Palimondo

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

Swift book, in The Basics chapter, section Implicitly Unwrapped Optionals 说:

当一个可选项的值在第一次定义后立即确认存在并且可以肯定地假定在此后的每个点都存在时,隐式展开的可选项很有用。 Swift 中隐式解包选项的主要用途是在类初始化期间,如无主引用和隐式解包选项属性中所述。 ...您可以将隐式解包的可选项视为允许在使用时自动解包该选项。不是每次使用时在可选项名称后放置一个感叹号,而是在声明它时在可选项的类型后放置一个感叹号。

这归结为属性的nil属性是通过使用约定建立的,并且在类初始化期间不能由编译器强制执行的用例。例如,从 NIB 或 Storyboard 初始化的 UIViewController 属性,其中初始化被拆分为单独的阶段,但在 viewDidLoad() 之后,您可以假设属性通常存在。否则,为了满足编译器的要求,您只能使用 forced unwrappingoptional bindingoptional 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


感谢您的详尽回答。你能想出一份关于何时使用隐式解包选项以及何时使用标准变量就足够的快速清单吗?
@Hairgami_Master 我用列表和具体例子添加了我自己的答案
r
rickster

单行(或多行)简单示例并不能很好地涵盖可选项的行为——是的,如果您声明一个变量并立即为其提供值,那么可选项就没有任何意义。

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

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 解决停机问题之前),所以你告诉编译器它存在。

不过,这在某种程度上破坏了类型安全。如果您的“保证”并不总是成立,那么您拥有隐式展开的可选选项的任何地方都是您的应用程序可能崩溃的地方,因此这是一个谨慎使用的功能。此外,一直使用 ! 会让您听起来像是在大喊大叫,而没有人喜欢这样。


为什么不将 screenSize 初始化为 CGSize(height: 0, width: 0) 并省去每次访问变量时都必须大喊大叫的麻烦呢?
大小可能不是最好的例子,因为 CGSizeZero 在实际使用中可能是一个很好的标记值。但是,如果您从 nib 加载的尺寸实际上可能为零怎么办?然后使用 CGSizeZero 作为标记并不能帮助您区分未设置的值和设置为零的值。此外,这同样适用于从 nib(或 init 之后的任何其他位置)加载的其他类型:字符串、对子视图的引用等。
函数式语言中 Optional 的部分要点是没有标记值。你要么有价值,要么没有。您不应该有一个值指示缺失值的情况。
我认为您误解了OP的问题。 OP 没有询问可选项的一般情况,而是专门询问隐式展开的可选项(即不是 let foo? = 42,而是 let foo! = 42)的需要/使用。这并没有解决这个问题。 (请注意,这可能是关于选项的相关答案,但不是关于隐式展开的选项,它们是不同/相关的动物。)
m
mfaani

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 属性声明为隐式展开的可选属性。


此答案中提到的教程是here
D
Danra

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

使用 !运算符,意味着您确定您的代码没有错误,并且可选项已经有一个正在解包的值。没有!运算符,您可能只需使用可选绑定进行断言:

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

这不如

println("\(value!)")

现在,隐式可选项让您可以在所有可能的流程中表达您希望在展开时始终具有值的可选项。所以它只是在帮助你方面更进一步——通过放宽编写!每次都打开包装,并确保运行时在您对流程的假设错误的情况下仍然会出错。


@newacct :所有可能的流程中的非零通过您的代码与从父(类/结构)初始化到释放的非零不同。 Interface Builder 是一个经典的例子(但还有很多其他延迟初始化模式):如果一个类只在一个 nib 中使用,那么出口变量将不会在 init 中设置(你甚至可能没有实现),但它们保证设置在 awakeFromNib / viewDidLoad 之后。
e
enadun

如果您确定从可选项而不是 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


这不能回答 OP 的问题。 OP 知道隐式展开的 Optional 是什么。
y
yoAlex5

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
}

p
pavel_orekhov

我认为 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...


关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅