首页 文章

什么是“致命错误:在展开可选值时意外发现nil”是什么意思?

提问于
浏览
328

我的Swift程序崩溃 EXC_BAD_INSTRUCTION 并出现此错误 . 它是什么意思,我该如何解决?

致命错误:在展开Optional值时意外发现nil


这篇文章旨在收集“意外发现的零”问题的答案,因此它们并不分散且难以找到 . 随意添加自己的答案或编辑现有的维基答案 .

8 回答

  • 547

    这个答案是社区维基 . 如果您觉得它可以做得更好,请随时编辑它!

    背景:什么是可选的?

    在Swift中,Optional是一个generic type,它可以包含一个值(任何类型),或者根本没有值 .

    在许多其他编程语言中,特定的"sentinel"值通常用于表示缺少值 . 例如,在Objective-C中, nilnull pointer)表示缺少对象 . 但是在使用原始类型时这会变得更加棘手 - 应该使用 -1 来表示缺少整数,或者可能是 INT_MIN ,还是其他整数?如果选择任何特定值来表示"no integer",则表示它不能再被视为有效值 .

    Swift是一种类型安全的语言,这意味着该语言可以帮助您清楚代码可以使用的值的类型 . 如果您的部分代码需要String,则类型安全性会阻止您错误地将其传递给Int .

    在Swift, any type can be made optional . 可选值可以采用原始类型的任何值,or特殊值 nil .

    选项在类型上使用 ? 后缀定义:

    var anInt: Int = 42
    var anOptionalInt: Int? = 42
    var anotherOptionalInt: Int?    // `nil` is the default when no value is provided
    

    nil 表示缺少可选值:

    anOptionalInt = nil
    

    (注意,这个nil与Objective-C中的nil不同 . 在Objective-C中,nil是缺少有效的对象指针;在Swift中,Optionals不限于对象/引用类型 . 可选行为类似于Haskell的也许 . )


    为什么我得到“致命错误:在打开可选值时意外发现nil”?

    为了访问可选的值(如果它有一个),你需要 unwrap 它 . 可以安全地或强制地打开可选值 . 如果强制解包一个可选项,并且它没有值,则程序将因上述消息而崩溃 .

    Xcode将通过突出显示一行代码来向您显示崩溃 . 此问题发生在此行上 .

    crashed line

    这种崩溃可以通过两种不同的强制解包方式发生:

    1.明确的力量展开

    这是通过可选的 ! 运算符完成的 . 例如:

    let anOptionalString: String?
    print(anOptionalString!) // <- CRASH
    

    由于 anOptionalString 在这里是 nil ,因此您将在强行打开它的行上发生崩溃 .

    2.隐式解包的选项

    这些是在 ! 之后定义的,而不是在类型之后的 ? .

    var optionalDouble: Double!   // this value is implicitly unwrapped wherever it's used
    

    假设这些选项包含一个值 . 因此,无论何时访问隐式展开的可选项,它都会自动为您强制解包 . 如果它不包含值,它将崩溃 .

    print(optionalDouble) // <- CRASH
    

    为了找出导致崩溃的变量,您可以在单击时按住⌥以显示定义,您可以在其中找到可选类型 .

    特别是IBOutlets通常是隐式解包的选项 . 这是因为您的xib或storyboard将在初始化后在运行时链接出口 . 因此,您应该确保在加载插件之前不要访问它们 . 您还应检查storyboard / xib文件中的连接是否正确,否则在运行时值将为 nil ,因此当它们是隐式时会崩溃解开 .


    我什么时候应该强行打开一个可选的?

    明确的力量展开

    作为一般规则,您不应该明确强制使用 ! 运算符解包可选项 . 可能存在使用 ! 是可接受的情况 - 但如果您100%确定可选项包含值,则应该只使用它 .

    虽然可能存在可以使用强制解包的情况,正如您所知道的可选项包含一个值 - 但是没有一个地方您无法安全地解开该可选项 .


    隐式解包的选项

    这些变量的设计使您可以将其分配推迟到代码的后面 . 在您访问它们之前,您有责任确保它们具有 Value . 但是,因为他们涉及力展开,它们本身仍然是不安全的 - 因为他们认为你的 Value 是非零的,即使分配nil是有效的 .

    您应该只使用隐式解包的选项作为最后的手段 . 如果您可以使用lazy variable,或为变量提供default value,则应该这样做,而不是使用隐式展开的可选项 .

    但是,有一个few scenarios where implicitly unwrapped optionals are beneficial,您仍然可以使用各种方式安全地解开它们,如下所示 - 但您应该始终谨慎使用它们 .


    如何安全地处理Optionals?

    检查可选项是否包含值的最简单方法是将其与 nil 进行比较 .

    if anOptionalInt != nil {
        print("Contains a value!")
    } else {
        print("Doesn’t contain a value.")
    }
    

    但是,99.9%的时间使用选项时,您实际上想要访问它包含的值,如果它包含一个 . 为此,您可以使用可选绑定 .

    可选绑定

    可选的Binding允许您检查可选项是否包含值 - 并允许您将展开的值分配给新变量或常量 . 它使用语法 if let x = anOptional {...}if var x = anOptional {...} ,具体取决于您是否需要在绑定后修改新变量的值 .

    例如:

    if let number = anOptionalInt {
        print("Contains a value! It is \(number)!")
    } else {
        print("Doesn’t contain a number")
    }
    

    这样做首先检查可选项是否包含值 . 如果是,则将“展开”值分配给新变量( number ) - 然后您可以自由使用它,就好像它是非可选的一样 . 如果optional不包含值,那么将调用else子句,如您所期望的那样 .

    什么是可选绑定,你可以同时打开多个选项 . 您可以使用逗号分隔语句 . 如果所有选项都被打开,该声明将成功 .

    var anOptionalInt : Int?
    var anOptionalString : String?
    
    if let number = anOptionalInt, let text = anOptionalString {
        print("anOptionalInt contains a value: \(number). And so does anOptionalString, it’s: \(text)")
    } else {
        print("One or more of the optionals don’t contain a value")
    }
    

    另一个巧妙的技巧是,你可以使用逗号在打开它之后检查值的某个条件 .

    if let number = anOptionalInt, number > 0 {
        print("anOptionalInt contains a value: \(number), and it’s greater than zero!")
    }
    

    在if语句中使用可选绑定的唯一方法是,您只能从语句的范围内访问unwrapped值 . 如果需要访问语句范围之外的值,可以使用guard语句 .

    guard statement允许您定义成功条件 - 并且当前范围仅在满足该条件时才会继续执行 . 它们使用语法 guard condition else {...} 定义 .

    因此,要将它们与可选绑定一起使用,您可以执行以下操作:

    guard let number = anOptionalInt else {
        return
    }
    

    (请注意,在保护体内,必须使用其中一个控制传输语句才能退出当前正在执行的代码的范围) .

    如果 anOptionalInt 包含一个值,它将被解包并分配给新的 number 常量 . 守卫之后的代码将继续执行 . 如果它不包含值 - 守卫将执行括号内的代码,这将导致控制权的转移,以便紧接着的代码不会被执行 .

    关于guard语句的真正优点是现在可以在语句后面的代码中使用展开的值(因为我们知道将来的代码只能在可选的值有值时执行) . 这对于消除嵌套多个if语句创建的‘pyramids of doom’非常有用 .

    例如:

    guard let number = anOptionalInt else {
        return
    }
    
    print("anOptionalInt contains a value, and it’s: \(number)!")
    

    Guards还支持if语句支持的相同的巧妙技巧,例如同时展开多个选项并使用 where 子句 .

    是否使用if或guard语句完全取决于将来的代码是否需要包含值的可选项 .

    无合并运算符

    Nil Coalescing Operatorternary conditional operator的漂亮简写版本,主要用于将期权转换为非期权 . 它的语法为 a ?? b ,其中 a 是可选类型, ba 的类型相同(尽管通常是非可选的) .

    它基本上让你说“如果 a 包含一个值,解开它 . 如果没有,则返回 b “ . 例如,您可以像这样使用它:

    let number = anOptionalInt ?? 0
    

    这将定义 Int 类型的 number 常量,如果包含值,则包含 anOptionalInt 的值,否则包含 0 .

    这只是简写:

    let number = anOptionalInt != nil ? anOptionalInt! : 0
    

    可选链接

    您可以使用Optional Chaining来调用方法或访问可选项上的属性 . 这可以通过在使用变量名时使用 ? 后缀来完成 .

    例如,假设我们有一个变量 foo ,类型为可选的 Foo 实例 .

    var foo : Foo?
    

    如果我们想在 foo 上调用一个方法不返回任何东西,我们可以简单地做:

    foo?.doSomethingInteresting()
    

    如果 foo 包含值,则将在其上调用此方法 . 如果没有,那么不会发生任何不好的事情 - 代码将继续执行 .

    (这与在Objective-C中将消息发送到nil的行为类似)

    因此,这也可用于设置属性以及调用方法 . 例如:

    foo?.bar = Bar()
    

    再说一次,如果 foonil ,这里不会发生任何不好的事 . 您的代码将继续执行 .

    可选链接允许您执行的另一个巧妙的技巧是检查设置属性或调用方法是否成功 . 您可以通过将返回值与 nil 进行比较来完成此操作 .

    (这是因为一个可选值将返回Void?而不是Void对一个不返回任何东西的方法)

    例如:

    if (foo?.bar = Bar()) != nil {
        print("bar was set successfully")
    } else {
        print("bar wasn’t set successfully")
    }
    

    但是,在尝试访问属性或调用返回值的方法时,事情变得有点棘手 . 因为 foo 是可选的,所以从它返回的任何内容也都是可选的 . 要解决这个问题,您可以使用上述方法之一解包返回的选项 - 或者在访问方法或调用返回值的方法之前解包 foo 本身 .

    此外,顾名思义,您可以将这些陈述“链接”在一起 . 这意味着如果 foo 具有可选属性 baz ,其属性 qux - 您可以编写以下内容:

    let optionalQux = foo?.baz?.qux
    

    同样,因为 foobaz 是可选的,所以从 qux 返回的值将始终是可选的,无论 qux 本身是否是可选的 .

    map和flatMap

    一个经常未充分利用的选项功能是能够使用 mapflatMap 函数 . 这些允许您将非可选变换应用于可选变量 . 如果可选项具有值,则可以对其应用给定的转换 . 如果它没有值,它将保持 nil .

    例如,假设您有一个可选字符串:

    let anOptionalString:String?
    

    通过将 map 函数应用于它 - 我们可以使用 stringByAppendingString 函数将其连接到另一个字符串 .

    因为 stringByAppendingString 采用非可选字符串参数,所以我们无法直接输入可选字符串 . 但是,通过使用 map ,如果 anOptionalString 具有值,则可以使用allow stringByAppendingString .

    例如:

    var anOptionalString:String? = "bar"
    
    anOptionalString = anOptionalString.map {unwrappedString in
        return "foo".stringByAppendingString(unwrappedString)
    }
    
    print(anOptionalString) // Optional("foobar")
    

    但是,如果 anOptionalString 没有值, map 将返回 nil . 例如:

    var anOptionalString:String?
    
    anOptionalString = anOptionalString.map {unwrappedString in
        return "foo".stringByAppendingString(unwrappedString)
    }
    
    print(anOptionalString) // nil
    

    flatMapmap 类似,但它允许您从闭包体中返回另一个可选项 . 这意味着您可以将一个可选项输入到需要非可选输入的进程中,但可以输出一个可选项 .

    试试!

    Swift的错误处理系统可以安全地与Do-Try-Catch一起使用:

    do {
        let result = try someThrowingFunc() 
    } catch {
        print(error)
    }
    

    如果 someThrowingFunc() 抛出错误,则会在 catch 块中安全地捕获该错误 .

    您在 catch 块中看到的 error 常量尚未由我们声明 - 它由 catch 自动生成 .

    您也可以自己声明 error ,它具有能够将其转换为有用格式的优点,例如:

    do {
        let result = try someThrowingFunc()    
    } catch let error as NSError {
        print(error.debugDescription)
    }
    

    使用 try 这种方式是尝试,捕获和处理来自抛出函数的错误的正确方法 .

    还有 try? 吸收了错误:

    if let result = try? someThrowingFunc() {
        // cool
    } else {
        // handle the failure, but there's no error information available
    }
    

    但是Swift的错误处理系统也为"force try"提供了一种方法 try!

    let result = try! someThrowingFunc()
    

    这篇文章中解释的概念也适用于此:如果抛出错误,应用程序将崩溃 .

    你应该只使用 try! ,如果你能证明它的结果永远不会在你的背景下失败 - 这是非常罕见的 .

    大多数情况下,您将使用完整的Do-Try-Catch系统 - 以及可选的一个, try? ,在极少数情况下处理错误并不重要 .


    资源

  • 11

    TL; DR回答

    使用very few exceptions,这个规则是金色的:

    避免使用!

    声明变量可选(?),而不是隐式解包的选项(IUO)(!)

    换句话说,而是使用:
    var nameOfDaughter: String?

    代替:
    var nameOfDaughter: String!

    使用if let或guard let解包可选变量

    要么解开变量,请执行以下操作:

    if let nameOfDaughter = nameOfDaughter {
        print("My daughters name is: \(nameOfDaughter)")
    }
    

    或者像这样:

    guard let nameOfDaughter = nameOfDaughter else { return }
    print("My daughters name is: \(nameOfDaughter)")
    

    这个答案旨在简明扼要,for full comprehension read accepted answer

  • 51

    这个问题出现在 ALL THE TIME 上 . 这是新Swift开发人员挣扎的第一件事 .

    背景:

    斯威夫特使用了“Optionals”的概念,用于处理可能包含值的值 . 在其他语言(如C)中,您可以在变量中存储值0,以指示它不包含任何值 . 但是,如果0是有效值怎么办?然后你可以使用-1 . 如果-1是有效值怎么办?等等 .

    Swift选项允许您设置任何类型的变量以包含有效值或无值 .

    当您声明变量意味着(键入x或没有值)时,在类型后面添加一个问号 .

    可选实际上是一个容器,而不是包含给定类型的变量,或者什么也不包含 .

    可选的需要“解包”才能获取内部值 .

    “!” operator是一个“force unwrap”操作符 . 它说:“相信我 . 我知道我在做什么 . 我保证当这段代码运行时,变量不会包含nil . ”如果你错了,你会崩溃 .

    除非你真的知道自己在做什么,否则请避免使用"!"强制解包操作符 . 它可能是开始Swift程序员的最大崩溃源 .

    如何处理期权:

    还有很多其他方法可以处理更安全的选项 . 这里有一些(不是详尽的清单)

    您可以使用“可选绑定”或“if let”来表示“如果此可选项包含值,请将该值保存到新的非可选变量中 . 如果可选项不包含值,请跳过此if语句的主体” .

    以下是使用 foo 可选项的可选绑定示例:

    if let newFoo = foo //If let is called optional binding. {
      print("foo is not nil")
    } else {
      print("foo is nil")
    }
    

    请注意,在if语句的主体中仅存在使用可选替换时定义的变量(仅在“范围内”) .

    或者,您可以使用guard语句,如果变量为nil,则可以退出函数:

    func aFunc(foo: Int?) {
      guard let newFoo = input else { return }
      //For the rest of the function newFoo is a non-optional var
    }
    

    在Swift 2中添加了Guard语句.Guard允许您通过代码保留“黄金路径”,并避免使用“if let”可选绑定有时导致嵌套ifs不断增加的级别 .

    还有一个称为“nil coalescing operator”的结构 . 它采用“optional_var ?? replacement_val”的形式 . 它返回一个非可选变量,其类型与可选中包含的数据相同 . 如果optional包含nil,则返回“??”后面的表达式的值符号 .

    所以你可以使用这样的代码:

    let newFoo = foo ?? "nil" // "??" is the nil coalescing operator
    print("foo = \(newFoo)")
    

    您也可以使用try / catch或guard错误处理,但通常上面的其他技术之一更清晰 .

    编辑:

    另一个稍微更精细的选项是“隐式解包的选项 . 当我们宣布foo时,我们可以说:

    var foo: String!
    

    在这种情况下,foo仍然是可选的,但您不必打开它来引用它 . 这意味着每当你尝试引用foo时,如果它为零则崩溃 .

    所以这段代码:

    var foo: String!
    
    
    let upperFoo = foo.capitalizedString
    

    即使我们没有强制解包foo,也会在引用foo的capitalizedString属性时崩溃 . 印刷品看起来很好,但事实并非如此 .

    因此,您希望对隐式解包的选项非常小心 . (甚至可能完全避免它们,直到你对选项有充分的了解 . )

    一句话:当你第一次学习Swift时,假装“!”角色不是语言的一部分 . 这可能会让你陷入困境 .

  • 11

    首先,您应该知道可选值是什么 . 你可以步骤The Swift Programming Launage

    细节 .

    其次,您应该知道可选值有两种状态 . 一个是完整值,另一个是零值 . 因此,在实现可选值之前,您应该检查它的状态 .

    您可以使用 if let ...guard let ... else 等 .

    另一种方法是,如果你在工具之前没有声明状态,你也可以使用 var buildingName = buildingName ?? "buildingName" .

  • 6

    由于上述答案清楚地解释了如何安全地使用Optionals . 我会尝试解释什么是Optionals真的很快 .

    声明可选变量的另一种方法是

    var i : Optional<Int>

    并且可选类型只是具有两种情况的枚举,即

    enum Optional<Wrapped> : ExpressibleByNilLiteral {
        case none 
        case some(Wrapped)
        .
        .
        .
    }
    

    所以给我们的变量'i'指定一个nil . 我们可以做 var i = Optional<Int>.none 或者分配一个值,我们会传递一些值 var i = Optional<Int>.some(28)

    根据swift,'nil'是没有 Value 的 . 并创建一个用 nil 初始化的实例我们必须遵循一个名为 ExpressibleByNilLiteral 的协议,如果您猜对了,那么只有 Optionals 符合 ExpressibleByNilLiteral 并且不鼓励遵守其他类型 .

    ExpressibleByNilLiteral 有一个名为 init(nilLiteral:) 的方法,用nil初始化一个实例 . 你通常不会调用这种方法,根据快速文档,我们不鼓励这样做每当初始化一个带有 nil literal的Optional类型时,编译器会调用它来直接调用此初始化程序 .

    甚至我自己也必须绕着Optionals:D Happy Swfting All包裹(没有双关语) .

  • 3

    当我尝试从准备segue方法设置我的Outlets值时,我遇到了这个错误,如下所示:

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if let destination = segue.destination as? DestinationVC{
    
            if let item = sender as? DataItem{
                // This line pops up the error
                destination.nameLabel.text = item.name
            }
        }
    }
    

    然后我发现我无法设置目标控制器出口的值,因为控制器尚未加载或初始化 .

    所以我这样解决了:

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if let destination = segue.destination as? DestinationVC{
    
            if let item = sender as? DataItem{
                // Created this method in the destination Controller to update its outlets after it's being initialized and loaded
                destination.updateView(itemData:  item)
            }
        }
    }
    

    目标控制器:

    // This variable to hold the data received to update the Label text after the VIEW DID LOAD
    var name = ""
    
    // Outlets
    @IBOutlet weak var nameLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
    
        // Do any additional setup after loading the view.
        nameLabel.text = name
    }
    
    func updateView(itemDate: ObjectModel) {
        name = itemDate.name
    }
    

    我希望这个答案可以帮助那些有同样问题的人,因为我发现明确的答案是理解选项及其工作方式的重要资源但是没有直接解决问题本身 .

  • -1

    基本上你试图在Swift只允许非零的地方使用nil值,通过“愚弄”编译器认为那里有nil值,从而允许你的app编译 .

    有几种情况会导致这种致命的错误:

    • 强制解包:
    let user = someVariable!
    

    如果 someVariable 为nil,那么你'll get a crash. By doing a force unwrap you moved the nil check responsibility from the compiler to you, basically by doing a forced unwrap you'保证编译器你永远不会有nil值 . 并猜猜如果发生这种情况会发生什么?

    解?使用可选绑定(又名if-let),在那里进行变量处理:

    if user = someVariable {
        // do your stuff
    }
    
    • 强迫演员:
    let myRectangle = someShape as! Rectangle
    

    在这里通过强制转换你告诉编译器不再担心,因为你总是有一个 Rectangle 实例 . 只要有,你就不必担心 . 当您或您的同事从项目开始循环非矩形值时,问题就开始了 .

    解?使用可选绑定(又名if-let),在那里进行变量处理:

    if let myRectangle = someShape as? Rectangle {
        // yay, I have a rectangle
    }
    
    • 隐式解包的选项 . 假设您有以下类定义:
    class User {
        var name: String!
    
        init() {
            name = "none yet"
        }
    
        func nicerName() {
            return "Mr/Ms " + name
        }
    }
    

    现在,如果没有人通过将 name 属性设置为 nil 来解决它,那么它会按预期工作,但是如果 User 是从缺少 name 键的JSON初始化的,那么在尝试使用该属性时会出现致命错误 .

    解?不要使用它们:)除非您确定该属性在需要使用时始终具有非零值 . 但是,在大多数情况下,将其转换为可选或非可选将起作用 . 使其成为非可选项也将导致编译器通过告知您错过的代码路径为该属性提供值来帮助您

    • 未连接的插座 . 这是场景#3的特例 . 基本上你有一些你想要使用的XIB加载类 .
    class SignInViewController: UIViewController {
    
        @IBOutlet var emailTextField: UITextField!
    }
    

    现在,如果您错过了从XIB编辑器连接插座,那么只要您想要使用插座,应用程序就会崩溃 . 解?确保所有插座都已连接 . 或者在它们上使用 ? 运算符: emailTextField?.text = "my@email.com" . 或者将插座声明为可选,但在这种情况下,编译器将强制您将其解包到代码中 .

    • 来自Objective-C的值,并且没有可空性注释 . 假设我们有以下Objective-C类:
    @interface User: NSObject
    @property NSString *name;
    @end
    

    现在,如果没有指定可空性注释(显式或通过 NS_ASSUME_NONNULL_BEGIN / NS_ASSUME_NONNULL_END ),那么 name 属性将作为 String! (一个IUO - 隐式展开的可选项)在Swift中导入 . 一旦一些swift代码想要使用该值,它将崩溃,在零的情况下 .

    解?为您的Objective-C代码添加可空性注释 . 但要注意,Objective-C编译器在可空性方面有点宽容,即使你明确地将它们标记为 nonnull ,也可能最终得到nil值 .

  • 35

    这是一个重要的评论,为什么隐含的解包选项在调试 nil 值时可能具有欺骗性 .

    考虑以下代码:它编译时没有错误/警告:

    c1.address.city = c3.address.city
    

    但在运行时它会出现以下错误:致命错误:在解开Optional值时意外发现nil

    Can you tell me which object is nil?

    你不能!

    完整的代码是:

    class ViewController: UIViewController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            var c1 = NormalContact()
            let c3 = BadContact()
    
            c1.address.city = c3.address.city // compiler hides the truth from you and then you sudden get a crash
        }
    }
    
    struct NormalContact {
        var address : Address = Address(city: "defaultCity")
    }
    
    struct BadContact {
        var address : Address!
    }
    
    struct Address {
        var city : String
    }
    

    长话短说使用 var address : Address! 你可能是一个变量可能来自其他读者的 nil . 当它崩溃时你就像“到底是什么?!我的 address 不是可选的,为什么我会崩溃?!

    因此,最好这样写:

    c1.address.city = c2.address!.city  // ERROR:  Fatal error: Unexpectedly found nil while unwrapping an Optional value
    

    Can you now tell me which object it is that was nil?

    这次代码已经变得更加清晰了 . 您可以合理化并认为可能是强制解包的 address 参数 .

    满满的代码是:

    class ViewController: UIViewController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            var c1 = NormalContact()
            let c2 = GoodContact()
    
            c1.address.city = c2.address!.city
            c1.address.city = c2.address?.city // not compile-able. No deceiving by the compiler
            c1.address.city = c2.address.city // not compile-able. No deceiving by the compiler
            if let city = c2.address?.city {  // safest approach. But that's not what I'm talking about here. 
                c1.address.city = city
            }
    
        }
    }
    
    struct NormalContact {
        var address : Address = Address(city: "defaultCity")
    }
    
    struct GoodContact {
        var address : Address?
    }
    
    struct Address {
        var city : String
    }
    

相关问题