浅谈Swift可选类型——茴字的四种写法

可选类型

Swift 文档将可选类型描述为:

使用 可选类型(optionals) 来处理值可能缺失的情况。可选类型表示两种可能: 或者有值,你可以解析可选类型访问这个值,或者根本没有值。

每种类型都有对应的可选类型,即使是自定义类型也如此。可选类型为每种类型提供了一个可以为空的选项,在 Swift 中空值为 nil。不同语言对于空值的处理不同,Python 中空值 None 被定义为一种类型,其动态类型的特性使得当一个对象被赋值为空时,解释器推定其类型为空类型。C 中有 void* 类型的 NULL 空指针。

Swift 中的 nil 是什么类型呢?答案是没有类型,其仅用来表示值的缺失。或者说其可以是任意可选类型。有趣的是,你不能在可选类型之间转换:

var voidInt: Int? = nil
var voidFloat: Float? = Float?(voidInt)
// error: cannot convert value of type 'Int?' to expected argument type 'Float'

如果强制解析呢?

var voidInt: Int? = nil
var voidFloat: Float? = Float?(voidInt!)
// error: cannot convert value of type 'Int' to expected argument type 'Float'

可以看到 voidInt 已经被强制解析为 Int 类型,但是仍然不能正确构造。这是因为可选类型只接受字面量构造。实际上应该先构造非可选类型,再赋给可选类型变量,因为非可选类型可以看作是可选类型的字集。

var voidInt: Int? = nil
var voidFloat: Float? = Float(voidInt!)
// crash when running

这时强制解析显然也是可以的,因为强制解析后变量被推定为非可选类型(从上一段代码的错误提示中也可见一斑),对于编译器来说这只是一个普通的类型转换。但是程序运行时会崩溃,因为强制解析了 nil 。不建议对可选类型做强制解析,除非你很确信它不会为空(既然这样为什么还要用可选类型呢?)。

var voidInt: Int? = nil
var voidFloat: Float? = Float(voidInt ?? 0)
// 0.0

使用空值结合(空合)处理可选类型可能为空的情况。在上面的代码中,voidFloat 最终值为0.0

用可选类型进行错误处理

下面的函数从数组中随机选取一个数:

func random(array: [Int]) -> Int {
  	return array.randomElement()
}

乍一眼看上去没有任何问题,但是我们不能保证 array 总存在。我们使用可选类型 [Int]? ,以防 array 值为 nil 导致崩溃。使用可选链处理 array 潜在的空值,只有当 array 存在时才从中随机取一个数。并用空合运算提供一个默认值,在 1 到 100 中选一个整数。

如果 array 存在但是为空呢?取随机仍然会失败。事实上内置的 randomElement() 方法返回的就是一个可选类型 Self?,以应对这种情况。

func selfishRandom(array: [Int]?) -> Int {
    return array?.randomElement() ?? Int.random(in: 1...100)
}
// change result without authorization
print(selfishRandom(array: nil))

假如你调用 selfishRandom() 并不小心传入一个空值或空数组(注意两者的区别,虽然两种情况函数的处理是相同的),你仍然会得到一个随机结果,这就仿佛函数“擅自”处理了这两种情况,令人困扰。反观 randomElement() 返回可选类型的设计是可取的,其返回一个可选类型,让我们可以自定义默认值。

func optionalRandom(array: [Int]?) -> Int? {
    return array?.randomElement()
}
// provide result that can be another type
print(optionalRandom(array: nil) ?? Int.random(in: 1...100).description)

现在 optionalRandom() 返回可选类型,让我们可以自主处理返回值为空的情况。例如上面的代码在将随机结果打印出来的时候才提供了默认值。

注意到计算属性 description 了吗?实际上它是 String 类型,之所以用 String 提供默认值是因为 print() 接受 Any 类型,事实上你不能将 print() 内的内容赋给一个变量(除非声明为 Any),因为 Swift 不能推断这个变量的类型,不能既要又要。

可选链和空值结合用十分简洁且直观的方法处理了所有可能出现的空值。但是不觉得过于简单粗暴了吗?在这两种情况中 array 为空值和为空的两种错误被混为一谈了,明明强调要注意两者的区别,却为了简洁直观放弃了具体问题具体分析的机会,只能说是简单直率了。

enum RandError: Error {
    case noArray, emptyArray
}

func failableRandom(array: [Int]?) throws -> Int {
    guard let array = array else {
        throw RandError.noArray
    }
    guard let result = array.randomElement() else {
        throw RandError.emptyArray
    }
    return result
}

使用可选绑定分别检查过程中的每一步,并可以抛出不同的错误,此时函数成为一个 throwing 函数。guard let ... elseif let 和写法都是可行的,不同之处是前者在解析为空时执行括号内代码,后者在解析有值时执行括号内代码。解析为空才发生错误,所以使用前者能够更专注于不发生错误的情况而将错误看作一个小插曲,毕竟在实际使用中非错误的函数本身执行的主要内容占绝大部分,我们不希望将大段内容缩进在括号里,而如果函数本身的主要任务就是错误处理,使用后者似乎会更合适。两种写法反映出心态上的微妙区别。

// handel errors
do {
    let result = try failableRandom(array: nil)
    print(result)
} catch RandError.noArray {
    print("At least give me an array!")
} catch RandError.emptyArray {
    print("As empty as my heart...")
}

// don't care what error
if let result = try? failableRandom(array: nil) {
    print(result)
} else {
    print("FailableRandom failed.")
}

可以对每种错误分别处理,当然也可以不区分错误类型,但至少这样的写法为我们提供了区分错误的机会。

Sorry potatoler, but I cannot recognize you.
Built with Hugo
Theme Stack designed by Jimmy