何时应该将可选值与nil进行比较?

46

很多时候,你需要编写如下的代码:

if someOptional != nil {
    // do something with the unwrapped someOptional e.g.       
    someFunction(someOptional!)
}

这似乎有点啰嗦,而且我听说使用强制解包运算符可能不安全,最好避免使用。有更好的处理方法吗?


2
冗长的?在C、Java或C++中使用了多年相同的。 - Sulthan
13
长寿和冗长是两个无关的概念。 - Airspeed Velocity
5个回答

115
几乎总是不必要检查可选项是否为nil。只有当你只关心它是否为nil时,才需要这样做-你不在意值的内容,只在意它不是nil

在大多数其他情况下,有一种Swift简写可以更安全、更简洁地在if内部完成任务。

如果它不是nil,则使用该值

而不是:

let s = "1"
let i = Int(s)

if i != nil {
    print(i! + 1)
}

您可以使用if let
if let i = Int(s) {
    print(i + 1)
}

您还可以使用 var

if var i = Int(s) {
    print(++i)  // prints 2
}

但请注意,i将是一个本地副本 - 对i的任何更改都不会影响原始可选项内部的值。
您可以在单个if let中取消包装多个可选项,并且后面的可选项可能会依赖于前面的可选项。
if let url = NSURL(string: urlString),
       data = NSData(contentsOfURL: url),
       image = UIImage(data: data)
{
    let view = UIImageView(image: image)
    // etc.
}

您可以在未包装的值中添加where子句:
if let url = NSURL(string: urlString) where url.pathExtension == "png",
   let data = NSData(contentsOfURL: url), image = UIImage(data: data)
{ etc. }

用默认值替换nil

不要这样写:

let j: Int
if i != nil {
    j = i
}
else {
    j = 0
}

或者:

let j = i != nil ? i! : 0

您可以使用空合并运算符??
// j will be the unwrapped value of i,
// or 0 if i is nil
let j = i ?? 0

将可选项与非可选项等同

不要这样写:

if i != nil && i! == 2 {
    print("i is two and not nil")
}

您可以检查可选项是否等于非可选值:
if i == 2 {
    print("i is two and not nil")
}

这也适用于比较:

if i < 5 { }

nil经常与其他nil相等,且小于任何非nil值。

注意!这里可能会有陷阱:

let a: Any = "hello"
let b: Any = "goodbye"
if (a as? Double) == (b as? Double) {
    print("these will be equal because both nil...")
}

在可选项上调用方法(或读取属性)

不要这样写:

let j: Int
if i != nil {
    j = i.successor()
}
else {
   // no reasonable action to take at this point
   fatalError("no idea what to do now...")
}

您可以使用可选链式调用,?.
let j = i?.successor()

请注意,现在j也是可选的,以应对fatalError情况。稍后,您可以使用本答案中的其他技术之一来处理j的可选性,但是您通常可以推迟实际取消包装可选项的时间,甚至根本不取消包装。
顾名思义,您可以将它们链接在一起,因此您可以编写:
let j = s.toInt()?.successor()?.successor()

可选链式调用也适用于下标操作:
let dictOfArrays: ["nine": [0,1,2,3,4,5,6,7]]
let sevenOfNine = dictOfArrays["nine"]?[7]  // returns {Some 7}

并且功能:

let dictOfFuncs: [String:(Int,Int)->Int] = [
      "add":(+),
      "subtract":(-)
]

dictOfFuncs["add"]?(1,1)  // returns {Some 2}

给可选项属性分配值

不要这样写:

if splitViewController != nil {
    splitViewController!.delegate = self 
}

您可以通过可选链来分配

splitViewController?.delegate = self

只有当splitViewController不为nil时,才会进行赋值操作。

如果它不是nil,则使用该值;或者在Swift 2.0中新加的可选退出语法

有时在一个函数中,您想要编写一小段代码来检查可选项,如果它是nil,则提前退出函数,否则继续执行。

您可以像这样编写:

func f(s: String) {
    let i = Int(s)
    if i == nil { fatalError("Input must be a number") }
    print(i! + 1)
}

或者为了避免强制解包,可以像这样编写代码:
func f(s: String) {
    if let i = Int(s) {
        print(i! + 1)
    }
    else { 
        fatalErrr("Input must be a number")
    }
}

但更好的方式是将错误处理代码保留在检查代码的顶部。这样做可以避免出现不愉快的嵌套("pyramid of doom")。

相反,您可以使用guard,它类似于 if not let

func f(s: String) {
    guard let i = Int(s)
        else { fatalError("Input must be a number") }

    // i will be an non-optional Int
    print(i+1)
}

else部分必须退出受保护值的范围,例如使用returnfatalError,以确保在其余范围内受保护的值是有效的。

guard不仅限于函数范围。例如以下代码:

var a = ["0","1","foo","2"]
while !a.isEmpty  {
    guard let i = Int(a.removeLast())
        else { continue }

    print(i+1, appendNewline: false)
}

打印321

在Swift 2.0中新增的非nil项循环

如果你有一个可选值的序列,你可以使用for case let _?来迭代所有非可选元素:

let a = ["0","1","foo","2"]
for case let i? in a.map({ Int($0)}) {
    print(i+1, appendNewline: false)
}

输出321。这是使用可选项的模式匹配语法,它由变量名称后跟?组成。

您还可以在switch语句中使用此模式匹配:

func add(i: Int?, _ j: Int?) -> Int? {
    switch (i,j) {
    case (nil,nil), (_?,nil), (nil,_?):
        return nil
    case let (x?,y?):
        return x + y
    }
}

add(1,2)    // 3
add(nil, 1) // nil

循环直到函数返回nil

if let类似,您也可以编写while let并循环直到nil

while let line = readLine() {
    print(line)
}

您也可以写成 while var(类似于 if var 的注意事项适用)。

where 子句也适用于此处(并终止循环,而不是跳过循环):

while let line = readLine() 
where !line.isEmpty {
    print(line)
}

将可选项传递到接受非可选项并返回结果的函数中。
而不是:
let j: Int
if i != nil {
    j = abs(i!)
}
else {
   // no reasonable action to take at this point
   fatalError("no idea what to do now...")
}

您可以使用Optional的map操作符:
let j = i.map { abs($0) }

这与可选链非常相似,但是当您需要将非可选值传递到函数作为参数时使用。与可选链一样,结果将是可选的。

当您需要一个可选项时,这非常好。例如,reduce1类似于reduce,但使用第一个值作为种子,在数组为空时返回一个可选项。您可以像这样编写它(使用先前提到的guard关键字):

extension Array {
    func reduce1(combine: (T,T)->T)->T? {

        guard let head = self.first
            else { return nil }

        return dropFirst(self).reduce(head, combine: combine)
    }
}

[1,2,3].reduce1(+) // returns 6

但是你可以使用map方法映射.first属性,并返回它:

extension Array {
    func reduce1(combine: (T,T)->T)->T? {
        return self.first.map {
            dropFirst(self).reduce($0, combine: combine)
        }
    }
}

将一个可选项传递到一个接受可选项并返回结果的函数中,避免烦人的双重可选项。
有时您想要类似于map的东西,但您想要调用的函数本身会返回一个可选项。例如:
// an array of arrays
let arr = [[1,2,3],[4,5,6]]
// .first returns an optional of the first element of the array
// (optional because the array could be empty, in which case it's nil)
let fst = arr.first  // fst is now [Int]?, an optional array of ints
// now, if we want to find the index of the value 2, we could use map and find
let idx = fst.map { find($0, 2) }

但现在idx的类型是Int??,是一个双重可选项。相反,您可以使用flatMap,它将结果“扁平化”为单个可选项:

let idx = fst.flatMap { find($0, 2) }
// idx will be of type Int? 
// and not Int?? unlike if `map` was used

8
虽然您提到了空值排序顺序,但构造“if i < 5 {}”可能会很危险,因为很容易决定更改逻辑并在将来的编辑中写入“if i >= 0 {}”。个人而言,我总是使用带可选项的“if let i = i where i < 5 {}”来进行判断。 - David H
很多这方面的内容都被提及了,几乎是逐字逐句地出自Chris Eidhof的《Advanced Swift》https://www.objc.io/books/advanced-swift/。 - user1951992
5
这句话的意思是,这篇文章的作者感觉像是那本书的作者之一也写了这篇文章。 - Airspeed Velocity
在Swift 3中,可选比较已被移除 - https://github.com/apple/swift-evolution/blob/master/proposals/0121-remove-optional-comparison-operators.md - Richard Groves

2

我认为你应该回到Swift编程书中学习这些内容的用途。当你确信可选项不为空时,使用感叹号。因为你声明了你是绝对确定的,如果你错了,它会崩溃。这完全是有意的。它在代码中是"不安全且最好避免"的,就像在你的代码中使用断言一样"不安全且最好避免"。例如:

if someOptional != nil {
    someFunction(someOptional!)
}

感叹号是绝对安全的。除非您的代码存在严重错误,例如误写代码(希望您能发现这个bug)

if someOptional != nil {
    someFunction(SomeOptional!)
}

如果出现这种情况,你的应用程序可能会崩溃,你需要调查崩溃原因并修复错误——这正是崩溃存在的原因。Swift 的一个目标是显然你的应用程序应该正常工作,但由于 Swift 无法强制执行此操作,它要求你的应用程序要么正常工作,要么在可能的情况下崩溃,以便在应用程序发布之前删除错误。


1
这篇自问自答的文章主要是为了回应在问题中(甚至在一些流行的Swift教程书籍中)经常出现的if someOptional != nil { use someOptional }。我也不同意使用!来让程序在nil时崩溃的做法是好的技巧。你最好使用带有明确错误消息的assert,而不是从!返回的神秘错误信息。 - Airspeed Velocity
2
可能这不是正确的做法,我发布这篇文章可能是错的,但绝对需要更多的规范答案来回答可选问题,例如“我如何使用<空合并运算符的完美用例>”等问题经常出现。 - Airspeed Velocity
第二个例子很可能会导致编译器错误,而不是实际的应用程序崩溃。 - return true
这并不是“绝对安全”的,因为无法保证您没有在块的开头意外地将变量设为 nil。您应该使用可选绑定。 - Alexander

0

有一种方法可以解决这个问题,它被称为可选链。根据文档:

可选链是一种查询和调用属性、方法和下标的过程,这些属性、方法和下标可能当前为nil。如果可选项包含一个值,则属性、方法或下标调用成功;如果可选项为nil,则属性、方法或下标调用返回nil。多个查询可以链接在一起,如果链中的任何一个链接为nil,则整个链会优雅地失败。

以下是一些示例:

class Person {
    var residence: Residence?
}

class Residence {
    var numberOfRooms = 1
}

let john = Person()

if let roomCount = john.residence?.numberOfRooms {
    println("John's residence has \(roomCount) room(s).")
} else {
    println("Unable to retrieve the number of rooms.")
}
// prints "Unable to retrieve the number of rooms."

您可以在这里查看完整的文章。


0
我们可以使用可选绑定。
var x:Int?

if let y = x {
  // x was not nil, and its value is now stored in y
}
else {
  // x was nil
}

0
经过深思熟虑和调研,我找到了最简单的解包可选项的方法:
  • 创建一个新的Swift文件并将其命名为UnwrapOperator.swift

  • 将以下代码粘贴到文件中:

    import Foundation
    import UIKit
    
    protocol OptionalType { init() }
    
    extension String: OptionalType {}
    extension Int: OptionalType {}
    extension Int64: OptionalType {}
    extension Float: OptionalType {}
    extension Double: OptionalType {}
    extension CGFloat: OptionalType {}
    extension Bool: OptionalType {}
    extension UIImage : OptionalType {}
    extension IndexPath : OptionalType {}
    extension NSNumber : OptionalType {}
    extension Date : OptionalType {}
    extension UIViewController : OptionalType {}
    
    postfix operator *?
    postfix func *?<T: OptionalType>( lhs: T?) -> T {
    
        guard let validLhs = lhs else { return T() }
        return validLhs
    }
    
    prefix operator /
    prefix func /<T: OptionalType>( rhs: T?) -> T {
    
        guard let validRhs = rhs else { return T() }
        return validRhs
    }
    
  • 现在上述代码已经创建了2个运算符[一个前缀和一个后缀]。

  • 在解包时,您可以在可选项之前或之后使用这两个运算符之一
  • 解释很简单,如果变量为空,则运算符返回构造函数值,否则返回变量内包含的值。

  • 下面是用法示例:

    var a_optional : String? = "abc"
    var b_optional : Int? = 123
    
    // 在使用运算符之前
    
    print(a_optional) --> Optional("abc")
    print(b_optional) --> Optional(123)
    
    // 前缀运算符用法
    
    print(/a_optional) --> "abc"
    print(/b_optional) --> 123
    
    // 后缀运算符用法
    
    print(a_optional*?) --> "abc"
    print(b_optional*?) --> 123
    
  • 下面是变量包含nil的示例:

    var a_optional : String? = nil
    var b_optional : Int? = nil
    
    // 在使用运算符之前
    
    print(a_optional) --> nil
    print(b_optional) --> nil
    
    // 前缀运算符用法
    
    print(/a_optional) --> ""
    print(/b_optional) --> 0
    
    // 后缀运算符用法
    
    print(a_optional*?) --> ""
    print(b_optional*?) --> 0
    
  • 现在由您选择使用哪个运算符,两者都具有相同的目的。


为什么您认为空初始化值(0""[]等)是合理的呢?可选项的全部意义在于强制开发人员考虑这一点,并思考处理非值的明智方式。例如,如果我有一个成绩计算应用程序,它取所有作业成绩的平均值,但还没有标记任何作业成绩,那么学生应该显示为0%的成绩吗?不! - Alexander
@Alexander,你对Swift中可选项的使用是正确的,但这个运算符是用于不同的用途的。大多数崩溃发生在强制向下转换或使用隐式解包可选项时,为了最小化这些类型的崩溃,可以使用上述提到的运算符。开发人员还必须清楚地知道何时使用该运算符。该运算符的使用仅是为了在nil的位置提供初始化值,就是这样。谢谢Alex :) - Mr. Bean
崩溃可能比允许无意义数据的传播更可取(故意编程了解包可导致致命错误的事实。很有可能情况是,解包Int?只会给你那个内存位置中的任何垃圾。但是,Swift团队有意实现了检查,以抛出致命错误,以防止出现这种不确定状态。) - Alexander
  1. 此运算符的目的已通过现有的 nil 合并运算符 (??) 实现。然而,关键区别在于,与您的 /*? 运算符不同,它不会代表程序员做出任何有关正确的“回退值”的假设。由他们自己决定。
- Alexander

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接