【问题标题】:Why create "Implicitly Unwrapped Optionals", since that implies you know there's a value?为什么要创建“隐式解包选项”,因为这意味着您知道存在价值?
【发布时间】:2014-07-23 07:19:40
【问题描述】:

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

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

会比:

let someString: String = "this is the string"

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

【问题讨论】:

    标签: swift design-patterns optional


    【解决方案1】:

    在我描述隐式解包选项的用例之前,您应该已经了解什么是 Swift 中的选项和隐式解包选项。如果你不这样做,我建议你先阅读my article on optionals

    何时使用隐式解包选项

    创建隐式解包选项的主要原因有两个。所有这些都与定义一个在 nil 时永远不会被访问的变量有关,否则,Swift 编译器将总是强制你显式地打开一个 Optional。

    1。初始化时无法定义的常量

    到初始化完成时,每个成员常量都必须有一个值。有时候,一个常量在初始化的时候不能用正确的值初始化,但是在被访问之前还是可以保证有值的。

    使用 Optional 变量可以解决这个问题,因为 Optional 会自动使用 nil 初始化,并且它最终包含的值仍然是不可变的。然而,不断解开一个你确定不是 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
        }
    }
    

    在这里,在视图加载之前,您无法计算按钮的原始宽度,但您知道 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.
    【解决方案2】:

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

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

    【讨论】:

      【解决方案3】:

      当一个属性确实需要在幕后是可选的时,隐式展开的可选项对于将属性显示为非可选非常有用。这对于在两个相关对象之间“打结”通常是必要的,每个对象都需要引用另一个对象。当两个引用实际上都是可选的,但其中一个引用在初始化对时需要为 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:你说得对——我已经编辑了答案以删除保留周期。我打算让反向引用变弱,但我想它溜走了。
      【解决方案4】:

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

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

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

      这归结为以下用例:属性的nil-ness 是通过使用约定建立的,编译器在类初始化期间无法强制执行。例如,从 NIB 或 Storyboard 初始化的 UIViewController 属性,其中初始化分为单独的阶段,但在 viewDidLoad() 之后,您可以假设属性通常存在。否则,为了满足编译器,您必须使用 forced unwrapping, optional 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 或自行解包。当您访问这种可选类型中的值而没有先安全地解包它时,隐式解包的选项会检查该值是否丢失。如果缺少该值,则会发生运行时错误。因此,您应该始终自己检查并解包隐式解包的可选项,除非您确定该值不会丢失。

      ...还有更远的地方

      【讨论】:

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

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

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

      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)。这并没有解决这个问题。 (请注意,这可能是关于选项的相关答案,但不是关于隐式展开的选项,它们是不同/相关的动物。)
      【解决方案6】:

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

      City 的初始化程序是从 Country 的初始化程序中调用的。但是,Country 的初始化程序无法将 self 传递给 City 初始化程序,直到完全初始化新的 Country 实例,如 Two-Phase Initialization 中所述。

      为了满足这一要求,您将CountrycapitalCity 属性声明为隐式展开的可选属性。

      【讨论】:

      • 本回答中提到的教程是here
      【解决方案7】:

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

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

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

      这不如

      println("\(value!)")
      

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

      【讨论】:

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

      如果您确定从可选项返回的值而不是 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。
      【解决方案9】:

      Implicitly Unwrapped Optional(IUO)

      它是Optional 的语法糖,不会强制程序员解包变量。它可用于在two-phase initialization process隐含非零期间无法初始化的变量。此变量的行为类似于 non-nil,但实际上是一个 可选 变量。一个很好的例子是——Interface Builder的出口

      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
      }
      

      【讨论】:

        【解决方案10】:

        我认为Optional 是一个让很多初学者感到困惑的结构的坏名字。

        其他语言(例如 Kotlin 和 C#)使用术语 Nullable,这样更容易理解。

        Nullable 表示您可以将空值分配给此类型的变量。所以如果是Nullable&lt;SomeClassType&gt;,你可以给它赋值,如果只是SomeClassType,你不能。这就是 Swift 的工作原理。

        为什么要使用它们?好吧,有时你需要有空值,这就是原因。例如,当您知道要在一个类中拥有一个字段,但在创建该类的实例时不能将其分配给任何东西,但稍后您会这样做。我不会给出例子,因为人们已经在这里提供了它们。我写这篇文章只是为了给我的 2 美分。

        顺便说一句,我建议你看看这在其他语言中是如何工作的,比如 Kotlin 和 C#。

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

        其他语言,如 Java 和 Scala 确实有 Optionals,但它们的工作方式与 Swift 中的 Optionals 不同,因为默认情况下 Java 和 Scala 的类型都可以为空。

        总而言之,我认为这个特性在 Swift 中应该被命名为 Nullable,而不是 Optional...

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2015-09-14
          • 1970-01-01
          • 2011-09-07
          相关资源
          最近更新 更多