Swift 可选类型Optional

Swift 可选类型Optional

[TOC]

前言

本将以Swift中的可选类型为入口,介绍:

  • 可选类型的底层实现
  • Swift中的nil
  • Optional的模式匹配
  • if语句以及强制解析
  • 可选绑定
  • 隐式解析可选类型等。

Swift中的Optional底层实现是enum,如果你对Swift中的enum不是很了解,可以先看看我的这篇文章Swift 枚举(enum)详解

1. Optional

1.1 简介

相对于apple开发的其他语言(C、Objective-C)中,Swift增加了可选(Optional)类型,用于处理值缺失的情况。简单来说,可选就是“那有一个值,并且它等于x”或者“那没有值”。可选有点像Objective-C中使用nil,但是它可以使用在任何类型上,不仅仅是类。可选类型比Objective-C中的nil指针更加安全也更具表现力,它是Swift许多强大特性的重要组成部分。

虽然说可选像Objective-C中使用nil,但是C和Objective-C中并没有可选类型的概念。nil也仅仅是针对没有一个合法的对象的时候使用,对于结构体,基本的C类型或者枚举类型不起作用。对于这些类型,Objective-C方法一般会返回一个特殊值(比如NSNotFound)来暗示缺失。这种方法假设方法的调用者知道并记得对特殊值进行判断。然而,Swift的可选类型可以让你暗示任意类型的值缺失,并不需要一个特殊值。

举个例子,当我们尝试将一个String转换成Int:

let possibleNumber = "123"
let convertedNumber = Int(possibleNumber)
// convertedNumber 被推测为类型 "Int?", 或者类型 "optional Int"

此时我们按住option键单击convertedNumber就可以看到它是Int?类型

image

因为上面的构造器可能会失败,如果possibleNumber的值为Hello World,就不会构造处一个Int,所以对于一个可选的Int应该被写作Int?。其中这里面的?相当于语法糖,Int?等价于optional

1.2 nil

我们可以给你可选变量赋值为nil来表示它没有值:

var serverResponseCode: Int? = 404
// serverResponseCode 包含一个可选的 Int 值 404
serverResponseCode = nil
// serverResponseCode 现在不包含值

但是nil不能用于非可选的常量和变量。如果我们的代码中有常量或者变量需要处理值缺失的情况,那就把它们声明成对应的可选类型。

如果声明一个可选常量或者变量但没有赋值,它们会自动设置为nil

var surveyAnswer: String?
// surveyAnswer 被自动设置为 nil\
print(surveyAnswer)


nil

Swift的nil和Objective-C中的nil并不一样。在Objective-C中,nil是一个指向不存在对象的指针。在Swift中,nil不是指针,它是一个确定的值,用来标识值缺失。任何类型的可选状态都可以被设置为nil,不只是对象类型。

1.3 Optional源码

查看Swift源码,此处使用的是Swift5.3.1,可以同command+p全局搜索Optional.Swift文件,来快速定位。由于同名文件很多,我们选择swift-source/swift/stdlib/public/core/Optional.swift路径下的Optional.Swift

@frozen
public enum Optional: ExpressibleByNilLiteral {
  // The compiler has special knowledge of Optional, including the fact
  // that it is an `enum` with cases named `none` and `some`.

  /// The absence of a value.
  ///
  /// In code, the absence of a value is typically written using the `nil`
  /// literal rather than the explicit `.none` enumeration case.
  case none

  /// The presence of a value, stored as `Wrapped`.
  case some(Wrapped)
  
  .....
  
}

通过源码我们可以知道Optional的本质使用一个具有关联值的enum,关联值的类型来自Optional所接收的一个泛型参数Wrapped

所以对于可选类型的写法,以下两种是等价的:

var a: Int? = 10
var a1: Optional = 10

1.4 Optional的模式匹配

既然Optional的本质是枚举,那么他也就可以使用枚举的模式匹配来匹配对应的值:

var a: Int? = 10

//匹配有值和没值两种情况
switch a {
    case .none:
        print("nil")
    case .some(let value):
        print(value)
}

// 匹配没值,有特定值的情况
switch a {
    case .none:
        print("nil")
    case .some(10):
        print("value is 10")
    default: print("other value")
}

1.5 解包

对于可选类型,我们要想使用的时候,其内部可能有值也可能没有值,所以我们需要对其进行解包。

1.5.1 if判断

我们可以使用if语句和nil比较来判断一个可选值是否包含值。可以使用==!=来比较

var a: Int? = 10

if a != nil {
    print("a has an integer value of \(a!).")
}

当我们确定可选包含一个非nil的值,就可以使用!来强制解析值了。

1.5.2 强制解析

最简单的写法就是强制解包了,写法简单,只需加一个感叹号!,但是也有坏处,就是一旦解包的值是nil,程序就会崩溃了:

image

就单从这一点来说,此方案能不用尽量别用,因为在Swift中每使用一个!都需要慎重。如果使用!进行解析,已读要去掉可选包含一个非nil的值。

1.5.3 可选绑定

如果不确定可选类型是否有值,我们通常会使用可选绑定进行判断:

  1. if let:如果有值,则会进入if 流程
  2. gurad let:如果为nil,则会进入else流程
var a: Int? = 10

if let value = a {
    print("a has an integer value of \(value).")
} else {
    print("a is nil")
}

func test() {
    guard let value = a else {
        print("a is nil")
        return
    }
    
    print("a has an integer value of \(value).")
}

test()
  • 一般我们使用if let的时候就是为了拿到可选类型的值进行业务逻辑处理,其值只在if分支中可用。
  • 我们使用guard let的时候是为了守护这个可选类型,当可选类型没有值,则优先进入else分支进行容错处理,如果有值,则后续都可以使用这个重绑定的值,而不需要在进行解包。

1.6 隐式解析可选类型

有时候在程序架构中,第一次赋值之后,可以确定一个可选类型总会有值。在这种情况下,每次都要判断和解析可选值是非常低效的,因为可以确定它总会有值。此时我们就可以通过隐式解析来解决这个问题,通常隐式解析可选类型被用于Swift中类的构造过程。

class Country {
    let name: String
    var capitalCity: String!
    init(name: String, capitalName: String) {
        self.name = name
        self.capitalCity = capitalName
    }
}

let country = Country(name: "China", capitalName: "beijing")
let capitalName = country.capitalCity

此时当country对象初始化后,其首都就已经确认了,在后续的使用的过程就不需要进行解包了。

但是由初始化的变量仍然是一个可选类型:

image

当然如果在init方法中没有给capitalCity赋值,在后续的使用的过程中与可选值为nil的时候使用!是一样的,会触发应用程序的崩溃错误。所以还是那句话,使用!需要谨慎。

我们可以把隐式解析可选类型当做普通可选类型类判断它是否包含值:

if country.capitalCity != nil {
    print(country.capitalCity!)
}

if let capital = country.capitalCity {
    print("The capital of \(country.name) is \(capital).")
} else {
    print("The capital of \(country.name) is nil")
}

func test() {
    guard let capital = country.capitalCity else {
        print("The capital of \(country.name) is nil")
        return
    }
    
    print("The capital of \(country.name) is \(capital).")
}

test()

注意:
如果一个变量之后可能变成nil的话请不要使用隐式解析可选类型。如果你需要在变量的生命周期中判断是否是nil的话,请使用普通可选类型

1.7 unsafelyUnwrapped

unsafelyUnwrapped即不安全的打开,是Swift Optional提供的一个计算属性,提供了与强制解包操作符!相同的值:

var a: Int? = 10

print(a!)
print(a.unsafelyUnwrapped)

如果可选值为nil,使用unsafelyUnwrapped通用会引起程序崩溃:

image

下面我们来看看unsafelyUnwrapped的源码:

  @inlinable
  public var unsafelyUnwrapped: Wrapped {
    @inline(__always)
    get {
      if let x = self {
        return x
      }
      _debugPreconditionFailure("unsafelyUnwrapped of nil optional")
    }
  }

在源码中我们可以看到是通过if let进行可选绑定,然后返回值,如果绑定失败则会打印错误信息。

虽然看起来跟!没什么区别,但是注释中有这么一句话:

image

在 -O 这个优化级别时,不会执行检测来确保当前实例实际上有一个值。

这里的-O是指target -> Build Setting -> Optimization Level设置成-O时。

下面我们就按照文档上说的,将优化级别设置成Fastest, Smallest[-Os],并将运行模式调整为release模式,重新运行:

PS: 首先Debug没生效,release也没生效,各种优化级别试了一遍也没生效。Fastest, Smallest[-Os] + release重启Xcode生效了。我用的是Xcode 12.2

image

所以在开发中如果需要强制解包就用!吧,按照官方文档说的此属性以安全换取性能,使用时机与!一致。

1.8 ?? 空合运算符

对于可选值来说,其值有可能为nil,此时我们想要给其赋一个默认值来解决值为nil的问题,如果通过解包等方法就显得很复杂了,此时使用??空合运算符,就会很简单,也使得代码很简洁。

空合运算符(a ?? b)将可选类型a进行空判断,如果a包含一个值就进行解包,否则就返回一个默认值b。表达式a必须是Optional类型。默认值b的类型必须要和a存储值的类型保持一致。

空合运算符是对以下代码的简短表达方法:

a != nil ? a! : b

以上是一个三元运算符,当可选值a不为空时,对其进行强制解包a!以访问a中的值;反之默认返回b的值。如果b是一个函数,也就不会执行了。

实际应用可以是这样:

var a: Int? = nil
print(a ?? getNum())

var b: Int? = 11
print(b ?? getNum())

func getNum() -> Int {
    print("get num")
    return 20
}


get num
20
11

当b有值的时候,是不会执行getNum函数的。

下面我们看看 ?? 的实现,在Optional源码中:

// 返回T
@_transparent
public func ?? (optional: T?, defaultValue: @autoclosure () throws -> T)
    rethrows -> T {
  switch optional {
  case .some(let value):
    return value
  case .none:
    return try defaultValue()
  }
}

// 返回T?
@_transparent
public func ?? (optional: T?, defaultValue: @autoclosure () throws -> T?)
    rethrows -> T? {
  switch optional {
  case .some(let value):
    return value
  case .none:
    return try defaultValue()
  }
}

根据源码,我们可以知道,??的返回值类型有两种,分别是TT?,这点主要与??后面的返回值有关,也就是??是什么类型,返回的就是什么类型。因为(a ?? b),b也有可能是可选类型。

image

此时我们可以看到c就是个Int?类型

image

此时dInt类型。

image

如果??后面的类型与前面的类型不匹配,则会报编译错误。Cannot convert value of type 'String' to expected argument type 'Int'无法将类型'String'的值转换为期望的参数类型'Int'。

1.9 可选链

可选链的意思就是允许在一个链上来访问当前属性/方法,示例:

class Person {
    var name: String?
    var subject: Subject?
}

class Subject {
    var subjectName: String?
    func test(){print("test")}
}

var s = Subject()
var p = Person()

if let sName = p.subject?.subjectName {
    print("subjectName is \(sName)")
} else {
    print("subjectName is nil")
}

p.subject?.test()

运行结果如下:


image

可选链当链上的任意一个值为nil则不会继续执行后面的代码。

2. 总结

至此我们对Swift的Optional的分析基本就到这里了,下面总结一下

  1. Optional是Swift增加的一种类型,用于处理值缺失的情况;
  2. 可选表示“那有一个值,并且它等于x”或者“那儿没有值”;
  3. Swift中的nil不是指针,它是一个确定的值,用来标识值缺失;
  4. Swift中任何类型的可选状态都可以被设置为nil,不只是对象类型;
  5. 在Objective-C中,nil是一个指向不存在对象的指针。
  6. Optional的本质是enum,所以它具备enum的特性,可以使用模式匹配来匹配
  7. 可选类型在使用的时候需要解包
    1. 可以使用最基本的if判断是否值为nil
    2. 可以使用!进行强制解包,但需要注意可能引起的崩溃问题
    3. 使用if let可选绑定,着重处理有值的情况
    4. 使用guard let可选绑定,守护值,着重处理没值的情况
  8. 对于在构造方法中赋值后不在为nil的属性,我们也可以隐式声明可选类型,已解决后续总需要解包的问题。
  9. Optional提供了一个与!功能一样的计算属性unsafelyUnwrapped
  10. unsafelyUnwrappedrelease环境Fastest, Smallest[-Os]优化级别,不会执行检查来确保当前私立实际上有一个值
  11. Optional提供了??空合运算符来便利的处理值为nil时提供默认值
  12. 对于可选链上的属性或者方法的调用,只要链上任意为nil则不会继续执行后面的代码

你可能感兴趣的:(Swift 可选类型Optional)