• 0

  • 476

关于 Swift 可选——你需要知道这些内容

1星期前

点赞评论,感觉有用的朋友可以关注笔者公众号 iOS 成长指北,持续更新好文章。

本文是 《Swift 100 Days》系列的的第 12 天, Swift1 100 Days 是笔者记录自己 Swift 学习的记录,欢迎各位指正。

解包 Nil 可选项 一文中,我们介绍 Swift 中的一种崩溃现象——强制解包 nil 可选项。

点赞评论,感觉有用的朋友可以关注笔者公众号 iOS成长指北,持续更新好文章。

在运行代码时,Xcode 会报错,下面介绍一个简单的报错现象。

var name: String? = ""

print(name!.count) //0

name = nil

print(name!.count) // Fatal error: Unexpectedly found nil while unwrapping an Optional value
复制代码

今天我们将完结关于 iOS 基础知识的学习。我们来解决这个令 Swift 开发者困扰的知识点 —— 可选项。

我们将了解它是什么,它为什么有用,以及如何使用它来使代码更安全、更少的 Bug、更易于维护。

Swift 中的 nil

在 Swift 中用 nil 来表示一个 不存在的值

Null references

在 1965 年,Tony Hoare 爵士 在编程语言中引入了这个被其称之为一个价值10亿美元的错误 的空指针。从此,在大多数的较老的命令语言(C、C++、java等)中,如何写出更安全的代码,都在令程序员欲仙欲死 ,为了避免触发

NullPointerException

这种错误与语言中的大多数其他类型错误不同,它在编译期间没有被捕获。某些语言可能会进行一些检查并发出警告,但不会提示这是个错误。

为了避免这种错误,从此,程序员开始笔耕不辍的在为自己的每个调用都加一个

if (x != nil) {

}
复制代码

即便如此,我们还是得小心翼翼,一旦我们遇到 NullPointerException 错误,还是需要花费大量时间去找到那个错误开始的地方。

哨值

表示特殊条件(如空值,0)的值被称为哨值, 例如当我们判断一个字符串是否可用时,我们判断当前字符串的长度是否大于 0,当我们不知道值为什么时,我们一般将变量的初始值定义成一个哨值

var string: String = ""
复制代码

其他编程语言中,空值通常是 哨值 概念的延伸,例如,iOS 开发者所熟悉的 Objective-C 中的 nil 的概念。 Objective-C 中的 nil 表示当前所 分配NSObject 的内容被设置为 zero

在Swift 中提供了一种相当优雅的方式将不存在的值的概念直接整合到语言中。Swift 提供了一个新的类型, 可选类型,来说明要么有一个 T 类型的值要么什么都没有

Optional

当使用可选值时,都会使用可选类型,即便我们从来没有用过 Optional。Swift 的类型系统通常用尾随的问号 ? 来简写 Optional,而不是显示完整的类型名。

let shortForm: Int? = Int("42")
let longForm: Optional<Int> = Int("42")
复制代码

Optional 的本质上就是一个包含两种情况的枚举

enum Optional<Wrapped> : ExpressibleByNilLiteral  {
  case none
  case some(Wrapped)
}
复制代码

所以对一个可选值进行赋值时,也可以使用

var name: String? = ""
name = Optional.none
if name == nil {
    print("Optional.none 和 nil 等值")
}
复制代码

当我们实现一个接受 Optional 参数的方法时,除了判断该值是否为空,我们也可以利用枚举判断来进行优化

func printName(_ name: String?) {
    switch name {
    case .some(let unwrappedValue):
        print("Name is \(unwrappedValue)")
    case .none:
        print("Name is nil")
    }
}
复制代码

Optional 是一个遵守 ExpressibleByNilLiteral 协议的的一种枚举, 官方文档中不建议我们让其他 **哨值 **通过协议一致性来支持这个 ExpressibleByNilLiteral 协议。

尝试造一下!试一试官方文档不让我们做的

enum MyTestOptional<Wrapped> : ExpressibleByNilLiteral {
  init(nilLiteral: ()) {
    print("要成为 nil 了")
    self = .none
  }
  init(_ some: Wrapped) {
    print("我被重新赋值了 \(some)")
    self = .some(some)
  }

  case none

  case some(Wrapped)
}

var test = MyTestOptional.some(Int("42"))
test = nil
test = MyTestOptional.init(Int("42"))

var testOptional: MyTestOptional<Int> = MyTestOptional.some(Int("42")!)
testOptional = nil
复制代码

通过查看文档我们还发现 Optional 还支持一些更加便捷的高阶函数。map()flatMap()

let sideLength: Int? = Int("20")
let possibleSquare = sideLength.map { $0 * $0 }
print(possibleSquare) // Prints: "Optional(400)"

var name: String? = "Antoine van der Lee"
let validName = name.flatMap { name -> String? in
    guard name.count > 5 else { return nil }
    return name
}
print(validName) // Prints: "Optional("Antoine van der Lee")"
复制代码

解包可选项

Swift 不会让我们在不解包的情况下使用可选项的值。在使用之前,我们需要解包可选项。

文章开头,介绍了_**强制解包 **_可选项会引起崩溃。那么什么是强制解包?我们应该如何安全的解包可选项呢?下面我们一一介绍。

强制解包(forced unwrapping)

可选项类型表明值要么有要么没有。Swift 允许开发者强制解包可选项,当我们能确定当前值不为 nil 时,将可选类型转换为非可选类型。

let num = Int("5")
let unwarppedNum = num!
复制代码

只有当你确定当前值不为 nil 时,你才可以进行强制解包可选项 —— 否则应用程序将发生崩溃。

我们需要一些安全的解包方法。一个安全的可选项解包需要满足以下两个条件

  • 取值 当我们需要使用可选项值的时候,可以提取当前值
  • 检查可用性 当可选项值为 nil,可以不处理

可选绑定(optional binding)

if letguard let 通常被称作 可选绑定(optional binding),我们将详细介绍这两种方法,并引申他们的变种。

if let

解包可选项的最常见的方法是使用 if let 语法,它是使用条件进行解包。如果可选项是有值的,那么我们可以在条件语句中使用,否则,条件失败。

var name: String? = nil

if let unwrapped = name {
    print("\(unwrapped.count) letters")
} else {
    print("Missing name.")
}
复制代码
guard let

Swift 为我们提供了一种替代 if let 的方法,称为 guard let。同样可以完成对可选项的检查和取值。通常,guard let 用于当检查失败时退出当前函数、循环或条件,它解包后的值将在检查之后继续保留。

func double(number: Int?) -> Int? {
	guard let number = number else {
		return nil
	}
	return number * 2
}
let input = 5
if let doubled = double(number: input) {
	print("\(input) doubled is \(doubled).")
}
复制代码
if varguard var

if var guard var 算是 if letguard let 的变种,如果我们需要在方法中修改可选项的值或其某个属性,用户后期处理某些东西,我们可以使用 if var guard var

var userName: String? = "需要点赞"

if var unwrapped = userName {
    print("\(unwrapped.count) letters")
    unwrapped = "iOS 成长指北"
} else {
    print("Missing name.")
}

print(userName ?? "")

func helloToUser(_ name: String?) {
    guard var unwrapped = name else {
        return
    }
    unwrapped = "iOS 成长指北"
    print("需要点赞\(unwrapped)")
}
print(userName ?? "")
复制代码

不过,请注意,所做的更改不会反映回原始可选项。

隐式解析可选类型(Implicitly unwrapped optionals)

这可能是解包可选项中最复杂的一种概念。

var implicitly: String! = nil
implicitly = "iOS 成长指北"
print(implicitly.count)
复制代码

当我们明确知道有一个值,在其被赋值以后,其永远不会在被赋值成为 nil 。如果,每次调用这个值,为了安全都进行一次可选绑定的话,这样的代码,冗余且低效。但是如果我们每次都进行强制解包的话,那么每次调用的时候,都需要在其后面跟一个!

当可选类型被第一次赋值之后就可以确定之后一直有值的时候,我们可以使用隐式解析可选类型来解决这个问题。我们可以把想要用作可选的类型的后面的 ? 改成 ! 来声明一个隐式解析可选类型。

一个常见的现象就是,当我们进行 Xib 进行界面绘制时,拖拽链接一个界面的组件,比如, 按钮,页面自动生成对应的代码。

@IBOutlet weak var button: UIButton!
复制代码

当页面未加载时,buttonnil, 一旦页面加载完成,button 将跟随着页面的生命周期,几乎不会再被重置为 nil。所以当我们在页面中使用 button 并为其赋值时,可以认为 button 不会为 nil

无论是可选项还是隐式解析可选类型,都是可选类型的一种。所以都可以使用可选绑定。

在我们手写页面组件元素时,我们应该如何写呢?当我们涉及页面时,会一一介绍。

nil 合并运算符

理论上来说,严格意义上 nil 合并运算符并不能算是解包可选项的一种,但是确实我们常用的处理可选项的方法。

let name: String? = "iOS 成长指北"
print(name?.count ?? 0)
复制代码

使用 ?? 为可选项值为 nil时,提供一个哨值。避免后面的方法使用了非可选项的值。

拓展可选

我们知道,Optional 的本质是一个遵循 ExpressibleByNilLiteral 协议的枚举。我们在拓展中提到过,我们可以为Optional 提供一个拓展。

extension Optional where Wrapped == String {
    var orEmpty: String {
        return self ?? "iOS 成长指北"
    }
}

var whereName: String? = "需要点赞"
print(whereName.orEmpty) // Prints: "Antoine van der Lee"
whereName = nil
print(whereName.orEmpty) // Prints: ""
复制代码

当我们使用使用字符串的值或方法时,我们可以使用 orEmpty 方法,不过前提是,定义的哨值不要成为自己的 累赘

可选链(Optional Chaining)

Swift 的可选链允许我们在一行代码中使用多个可选项,如果其中任何一个值是 nil,那么当前语句的返回值就为 nil。通过这种链式调用,我们可以在当前值可能为 nil 的可选项上请求和调用属性、方法及下标的方法。

属性

我们可以使用 可选链 来获取或设置属性。

class DriversLicence {
    var pointsOnLicence = 0
}

class Person {
    var licence : DriversLicence?
}

var tom: Person? = nil

print(tom?.licence?.pointsOnLicence ?? 0)  // Prints: 0

tom?.licence?.pointsOnLicence = 10

print(tom?.licence?.pointsOnLicence ?? 0) // Prints: 0
复制代码

当我们存在一个可选对象,并且其对象属性为一个可选属性时,我们可以通过使用可选链来进行读取和赋值。

可选绑定

当我们在使用_可选链_来获取属性时,推荐和可选绑定一起使用

if let point = tom?.licence?.pointsOnLicence {
    print("\(point)")
}

func testPerson(_ person: Person?) {
    guard let point = person?.licence?.pointsOnLicence else {
        return
    }
    print("\(point)")
}
复制代码
访问下标

使用可选链访问下标包括两种,一种是下标方法返回的是可选项,例如,字典获取某一个 key 值,另外一种是取下标的对象就是一个可选项,比如这个字典就是可选的。

var info = ["fans" : (min:10, max:10000000)]

if let number = info["fans"]?.min {
    print("The number of 公众号:iOS成长指北 \(number)")
}
复制代码

之所以分成两种类型,其实就是一个关于 ? 在前在后的问题。

var info: Dictionary? = ["fans" : (min:10, max:10000000)]

if let number = info?["fans"]?.min {
    print("The number of 公众号:iOS成长指北 \(number)")
}
复制代码
可选绑定

在可选绑定一节,我们介绍了关于使用可选绑定进行赋值的操作,只能在作用域之内用。但是如果是下标的可选链访问,我们能不能进行赋值操作呢?

if let number = info?["fans"]?.min = 100 {
    print(type(of: number)) // ()
}
print(info as Any) //Prints:Optional(["fans": (min: 100, max: 10000000)])

复制代码

当通过可选链设置一个属性时,返回一个可选的元组(Void?()?

方法

可以通过可选链式调用来调用方法,并判断是否调用成功,即使这个方法 没有返回值

我们拓展一下之前的例子,然后我们来看看我们如何使用可选链和可选绑定。

当有返回值的时候,绑定获取的值,否则绑定一个默认的返回值 (),我们知道一个没有返回值的函数它的类型是 () -> ()

class DriversLicence {
    var pointsOnLicence = 0
    
    func changePointsOnLicence() {
        self.pointsOnLicence = 2
    }
}

class Person {
    var licence : DriversLicence?
}

var tom: Person? = Person()
tom?.licence = DriversLicence()
if let point = tom?.licence?.changePointsOnLicence() {
    print(type(of: point)) //Prints:()
}

复制代码
可选协议方法

如果你有 Objective-C 的经验,你可能会错过可选的协议方法。虽然 Swift 中有更好的方法来编写可选协议方法,但标准库中最常这么操作:

@objc protocol UITableViewDataSource : NSObjectProtocol {

    @objc optional func numberOfSections(in tableView: UITableView) -> Int

    // ...
}

let tableView = UITableView()
let numberOfSections = tableView.dataSource?.numberOfSections?(in: tableView)
复制代码

多级可选链

可以通过连接多个可选链式调用在更深的模型层级中访问属性、方法以及下标。

关于多级可选链我们需要知道的是,可选链的返回值为要么确定的值,要么是 nil ,所以使用多级可选链返回的是一个 *? 而不是 *??

常见用法

除了使用可选项来表明的对象可以,我们还有一些常见的其他用法

将错误转换成可选值(Optional try)

可以使用 try? 通过将错误转换成一个可选值来处理错误。如果是在计算 try? 表达式时抛出错误,该表达式的结果就为 nil

当我们使用一个异常捕捉的方法时,我们一般这么使用

enum PasswordError: Error {
    case obvious
}

func checkPassword(_ password: String) throws -> Bool {
    if password == "password" {
        throw PasswordError.obvious
    }

    return true
}

do {
    try checkPassword("password")
    print("That password is good!")
} catch {
    print("You can't use that password.")
}
复制代码

当是我们可以将抛出的错误转换成可选值,我们可以使用

if let result = try? checkPassword("password") {
    print("Result was \(result)")
}else {
    print("You can't use that password.")
}
复制代码

Objective-C 开发者在开发过中很少用到 try catch 语句。也许,使用 try? 更加符合你的习惯。

try? 对应的就是 try!,如果你确定你的异常一定不会出现,你可以通过这个方式,否则就如同强制解包一样,一旦异常发生,就会发生崩溃。

可能这不失为一个测试手段,但是绝对不要在生产环境使用。毕竟崩溃比错误更可怕

可失败构造器(Failable initializers)

在协议与拓展一章节中,我们建议在协议中如果要在协议中声明构造器,建议声明一个可失败构造器

理论上来说,一个合格的构造器在运行结束以后应该生成一个实例。但是如果我们有什么值使我们不可以没有的,或者我们的对象中某个值一定要满足某个条件,例如我们的身份证。那么我们可以初始化一个可失败构造器(Failable initializers),返回值为nil

struct Person {
    var id: String

    init?(id: String) {
        if id.count == 16 {
            self.id = id
        } else {
            return nil
        }
    }

}
复制代码

类型转换(Typecasting)

在 Swift 编程过程中,你需要时刻保持对类型的敏感度。你必须明确你变量的类型。

我们知道 Swift 中存在继承,也存在基于某个协议来作为数组、字典或其他容器中的元素类型。

我们定义一个数组

class Animal { }
class Fish: Animal { }

class Dog: Animal {
    func makeNoise() {
        print("Woof!")
    }
}

let pets = [Fish(), Dog(), Fish(), Dog()]
复制代码

不同的类中实现了不同的方法

如果我们有一个循环来遍历我们数组中的对象,并且针对不同类型,调用不同方法。

as? 同样可以用在可选绑定中。

for pet in pets {
    if let dog = pet as? Dog {
        dog.makeNoise()
    }
}
复制代码

当在 Swift 中输入强制转换值时,你可以检查该值的类型,并且可以在其自己的类层次结构中将其视为不同的类型。通常在子类和超类之间相关类型之间进行强制转换。

其他用法

我们在介绍一些其他的用法,这些用法可能不那么的常见

嵌套可选(Nested optionals)

在多级可选链中我们提到一个*?? 的概念。对一个可选值进行嵌套解包。虽然多级可选链并不会生成一个嵌套可选,但是在实际工作过程中,我们是有机会写出嵌套可选,使用嵌套可选时需要注意实现嵌套解包。

嵌套可选是一个历史问题,具体我们可以查看 SE-0230 – Flatten nested optionals resulting from ‘try?’

当我们这么写的话

let nameAndAges: [String:Int?] = ["Antoine van der Lee": 28]
let antoinesAge = nameAndAges["Antoine van der Lee"]
print(antoinesAge as Any) // Prints: "Optional(Optional(28))"
print(antoinesAge! as Any) // Prints: "Optional(28)"
print(antoinesAge!!) // Prints: "28"
print(type(of: antoinesAge)) // Prints: Optional<Optional<Int>>
复制代码

一定要相当注意我们关于可选的实现,否则会出现问题。

XCTUnwrap 编写一个可选的单元测试

当编写单元测试的时候,有一个很好的方法来处理可选项——而不是使用强制解包。

你可以使用 XCTUnwrap,如果可选参数不包含值,它会抛出错误:

func testBlogPostTitle() throws {
    let blogPost: BlogPost? = fetchSampleBlogPost()
    let unwrappedTitle = try XCTUnwrap(blogPost?.title, "Title should be set")
    XCTAssertEqual(unwrappedTitle, "Learning everything about optionals")
}
复制代码

map,compactMap 和 flatMap

使用转换将值集合映射到新值数组中。Swift 标准库为这种映射提供了三个主要 api——map、flatMap和compactMap。

我们一般这么写来获取一个请求

func makeRequest(forURLString string: String) -> URLRequest? {
    guard let url = URL(string: string) else {
        return nil
    }
    return URLRequest(url: url)
}
复制代码

但是,其实我们也可以使用map 方法

func makeRequest(forURLString string: String) -> URLRequest? {
    URL(string: string).map { URLRequest(url: $0) }
}
复制代码

善用枚举

当我们使用一个存在可选的枚举值,我们处理可枚举的值的时候,我们可以这么

enum Relationship {
    case friend
    case family
    case coworker
}

let relationship: Relationship? = nil

switch relationship {
case .some(let relationship):
    switch relationship {
    case .friend:
        print("friend")
    case .family:
        print("family")
    case .coworker:
        print("coworker")
    }
case .none:
    print("nil")
}
复制代码

当然我们也可以用一个 case nil 来处理这个

switch relationship {
  case .friend:
  print("friend")
  case .family:
  print("family")
  case .coworker:
  print("coworker")
  case nil:
  print("nil")
}
复制代码

感谢你阅读本文! 🚀

参考资料

Apple Developer 的 Optional

100 Days of Swift - Optionals

Optional Chaining in Swift

Optionals

Optionals in Swift explained: 5 things you should know

免责声明:文章版权归原作者所有,其内容与观点不代表Unitimes立场,亦不构成任何投资意见或建议。

ios

476

相关文章推荐

未登录头像

暂无评论