【函数式 Swift】可选值

本文探讨Swift中的可选值特性,通过案例展示如何利用可选值解决Objective-C中nilornotnil的问题,实现更安全、清晰的代码。

可选值(Optionals)是 Swift 引入的一项非常棒的特性,本文将基于原书中的案例以及函数式编程思想进行讨论。


概述

首先推荐大家阅读原书中的可选值章节,有很多使用细节以及与 Objective-C 的对比讨论,我认为如果将这一章单独拿出来会是一篇讲解 Swift 可选值很不错的文章。

关于 Swift Optionals,我曾写过一篇《Swift Optionals 源码解析》,试图借助 Swift 源码来解析可选值,其中开篇提到:

Swift 引入的 Optionals,很好的解决了 Objective-C 时代 “nil or not nil” 的问题,配合 Type-Safe 特性,帮我们减少了很多隐藏的问题。

这里主要有两方面意思:

  • “nil or not nil”:在 Objective-C 中,nil 是一个极其常见的值,指向一个为空的对象,很重要的是向 nil 发消息是安全的,回忆一下我们曾经写过的代码,有意无意的都使用了该特性,换句话说,有很多情况我们是不对为空做区别处理的,这样一来,我们的代码通常会简洁一些,可是一旦由于 nil 的存在或是传递导致了 Crash,就不得不去追查任何可能为 nil 的情况。
  • “Type-Safe”:Swift 是一个类型安全的语言,我们需要清楚的了解被操作的值的类型,编译器会对代码时进行类型检查,把不匹配的类型标记为错误,这样能够帮助我们尽早的发现程序中的问题。而对于类似 Objective-C 中的 nil 来说,一定程度与类型安全产生了冲突,因而,引入 Optionals 来与 Type-Safe 配合很好的解决这一问题。

下面将结合原书案例进一步讨论。

注:本文将不涉及对可选值基础应用的讲解,如需学习,请查阅原书官方文档《Swift Optionals 源码解析》

案例:Dictionary

使用两个 Dictionary 分别存储国家及其首都、城市名及其人口数量:

let capitals = [
    "France": "Paris",
    "Spain": "Madrid",
    "The Netherlands": "Amsterdam",
    "Belgium": "Brussels"
]

let cities = ["Paris": 2241, "Madrid": 3165, "Amsterdam": 827, "Berlin": 3562] // 单位为“千”复制代码

问题:编写函数接收输入的国家名,输出其首都城市的人口数量。

与之前章节一样,问题本身并不复杂,为了对比,我们先编写一个 Objective-C 版本的函数:

@property (nonatomic, copy) NSDictionary *capitals;
@property (nonatomic, copy) NSDictionary *cities;

_capitals = [[NSDictionary alloc] initWithObjectsAndKeys:
            @"Paris", @"France",
            @"Madrid", @"Spain",
            @"Amsterdam", @"The Netherlands",
            @"Brussels", @"Belgium",
            nil];

_cities = [[NSDictionary alloc] initWithObjectsAndKeys:
            @2241, @"Paris",
            @3165, @"Madrid",
            @827, @"Amsterdam",
            @3562, @"Berlin",
            nil];

- (NSInteger)populationOfCapital:(NSString *)country {
    return [_cities[_capitals[country]] integerValue] * 1000;
}复制代码

populationOfCapital 函数就是我们的解决方案,只需要一行代码即可,下面测试一下:

// Case 1
NSLog(@"%ld", [self populationOfCapital:@"France"]); // 2241000

// Case 2
NSLog(@"%ld", [self populationOfCapital:@"China"]); // 0复制代码

对于 Case 1 一切正常,而对于 Case 2,由于 _capitals 中不包含 China,所以 _capitals[country]nil,按照 Objective-C 的 nil 处理办法,我们得到了 0,但是,我们无法区分这个是正常的结果还是异常的输出,如果我们修改一下 _capitals_cities

_capitals = [[NSDictionary alloc] initWithObjectsAndKeys:
            @"Paris", @"France",
            @"Madrid", @"Spain",
            @"Amsterdam", @"The Netherlands",
            @"Brussels", @"Belgium",
            @"Tokyo", @"Japan", // Add Japan
            nil];

_cities = [[NSDictionary alloc] initWithObjectsAndKeys:
            @2241, @"Paris",
            @3165, @"Madrid",
            @827, @"Amsterdam",
            @3562, @"Berlin",
            @0, @"Tokyo", // Add Tokyo
            nil];

// Case 3
NSLog(@"%ld", [self populationOfCapital:@"Japan"]); // 0复制代码

我们忽略 Tokyo 是不是真的“零人口”,Case 3 输出 0 是正确的,但我们无法区分 Case 2 和 Case 3 的输出。这也就是 “nil or not nil” 问题。

那么 Swift 如何使用 Optional 解决这个问题呢?我们知道 populationOfCapital 函数应该能够区分 nilnot nil 的情况,而这正好匹配了可选值的定义:

// Optional.swift(swift/stdlib/public/core/Optional.swift)
public enum Optional<Wrapped> : ExpressibleByNilLiteral {
    case none // nil
    case some(Wrapped) // not nil
}复制代码

因此,populationOfCapital 的返回值类型应为 Int?capitalscities 查询失败时对应 nil 的情况,查询成功则对应 not nil,所以 Swift 中 populationOfCapital 函数定义如下:

func populationOfCapital(country: String) -> Int? {
    if let capital = capitals[country] {
        if let population = cities[capital] {
            return population * 1000
        } else {
            return nil
        }
    } else {
        return nil
    }
}

let cities = [
    "Paris": 2241, 
    "Madrid": 3165, 
    "Amsterdam": 827, 
    "Berlin": 3562, 
    "Tokyo": 0
]

let capitals = [
    "France": "Paris",
    "Spain": "Madrid",
    "The Netherlands": "Amsterdam",
    "Belgium": "Brussels",
    "Japan": "Tokyo"
]

// Case 4
populationOfCapital(country: "France") // 2241000
populationOfCapital(country: "China") // nil
populationOfCapital(country: "Japan") // 0复制代码

可以看到,借助可选值,Case 4 的输出满足了我们的需求,China 和 Japan 的情况也得到了区分。问题已经解决了,不过 populationOfCapital 完成的不够“美观”,也并没有利用到函数式编程思想,下面对它进行优化。

首先,我们先来看看 populationOfCapital 到底要完成什么工作,本质上就是从一个值转变为另一个值,即从国家名转变为人口数量,只是在转变过程中可能存在“失败”的情况,需要对这些“失败”的情况作出判断,通过第三章,我们知道值的转变对应的就是 map 方法,所以我们需要开发的工具库就是支持可选值的 map 方法,称之为:flatMap

要为 Optional 添加 flatMap 函数,并且存在“失败”的情况,所以 flatMap 函数的输出类型应为 U?,而输入,则应该是该可选值不为 nil 时所要做的转换(转换本身也是一个函数):

extension Optional {
    func flatMap<U>(f: Wrapped -> U?) -> U? {
        guard let x = self else {
            return nil
        }
        return f(x)
    }
}复制代码

工具库就绪后,我们可以改写 populationOfCapital 函数:

func populationOfCapital_map(country: String) -> Int? {
    return capitals[country].flatMap { capital in
        return cities[capital]
    }.flatMap { population in
        return population * 1000
    }
}

// Case 5
populationOfCapital_map(country: "France") // 2241000
populationOfCapital_map(country: "China") // nil
populationOfCapital_map(country: "Japan") // 0复制代码

借助工具库,我们不再需要进行多次判断,而是直接关注“成功”的情况即可。事实上,在 Optional 类库中已经存在一个同样功能的 flatMap 函数,开发中直接调用即可,其源码如下:

// Optional.swift(swift/stdlib/public/core/Optional.swift)
public func flatMap<U>(_ transform: (Wrapped) throws -> U?)
    rethrows -> U? {

    switch self {
        case .some(let y):
            return try transform(y)
        case .none:
            return .none
    }
}复制代码

为什么使用可选值?

原书中详细讨论了为什么 Swift 使用可选值,大致总结以下两点:

  1. 安全特性:不同于 Objective-C 默认近似零的处理方式,Swift 通过显式的可选类型以及类型安全特性,帮助避免由于缺失值而导致的意外崩溃或是逻辑含糊;
  2. 明确的函数签名:Objective-C 中函数的参数并无是否为空的强制要求,我们在使用时往往也比较模糊,仿佛空和非空均支持(这种情况目前有所改观,借助 nullableNS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END 能够帮助我们设计空和非空的情况),而由于可选值的存在,使得 Swift 中的函数签名是否支持 nil 变得非常清晰,更有利于我们写出健壮的代码。

参考资料

  1. Github: objcio/functional-swift
  2. The Swift Programming Language: The Basics
  3. The Swift Programming Language (Source Code)

本文属于《函数式 Swift》读书笔记系列,同步更新于 huizhao.win,欢迎关注!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值