Main Nightly

OptionalAPI

Swift Optional Monad 的可选扩展... 用不用... 它是可选的。

为什么

在 Swift 中使用 Optionals 时,会出现一些常见的习惯用法。这里有一些针对某些类型的有用扩展。

安装

只需复制粘贴文件到你的项目 🍝

或者使用 SPM 😎

文档

GitHub 页面: OptionalAPI

示例

如果为 none 或 some 时运行一些代码

someOptional == nil ? True branch : False branch

someOptional.isSome ? True branch : False branch
someOptional.isNone ? True branch : False branch

someOptional.isNotSome ? True branch : False branch
someOptional.isNotNone ? True branch : False branch

可能也返回 optional 的操作序列

返回 optional 的操作

func maybeIncrement(_ i: Int) -> Int? { i + 1 }

旧的糟糕方式

if let trueInt = someIntOptional {
    let incrementedOnce = maybeIncrement(trueInt) {
        // you get the idea ;)
    }
}

andThen

someOptional
    .andThen(maybeIncrement)
    .andThen(maybeIncrement)
    // ... you get the idea :)

在这种情况下,此链式调用的结果是 Int? 的实例。如果 someOptional 为 nil,则整个计算结果为 nil。如果它有一些值 (42),那么它将被递增这么多次。

none 情况恢复

假设你有一系列操作,并且结果可能返回 none

func returningNone(_ i: Int) -> Int? { Bool.random() ? .none : i }

someOptional
    .andThen(maybeIncrement)
    .andThen(returningNone)  // <-- returns nil
    .andThen(maybeIncrement)

最终结果是 nil。你不能使用 ??。使用 mapNone,它就像 Optional 上的普通 map,但用于 nil 情况。

func returningNone(_ i: Int) -> Int? { .none }

someOptional
    .andThen(maybeIncrement)
    .andThen(returningNone)
    .mapNone(42)
    .andThen(maybeIncrement)

如果 someOptional10 开头,并且我们很幸运(returningNone 没有返回 nil),那么最终结果是 12。但如果我们不那么幸运,那么 mapNone 将接管,最终结果将是 43

你还可以使用多个 mapNone 来处理沿途的任何失败。哦,你可以使用更友好的名称 defaultSome,就像这样:

someOptional
    // if someOptional is nil then start computation with default value
    .defaultSome(5)
    // increment whatever is there
    .andThen(maybeIncrement)
    // are you feeling lucky?
    .andThen(returningNone)
    // cover your ass if you had bad luck
    .defaultSome(42)
    // do some work with what's there
    .andThen(maybeIncrement)
    // what... again
    .andThen(returningNone)
    // saved
    .defaultSome(10)

我希望你能看到,这为你提供了一个非常灵活的 API 来处理你代码中的 Optionals。

andThenTry

此运算符期望一个可能抛出错误的转换。当这种情况发生时,它返回 .none,这允许使用其他运算符进行恢复。

let jsonData: Data? = ...

jsonData
    .andThenTry{ data in
        try JSONDecoder().decode(CodableStruct.self, from: data)
    }
    // this can also explode!
    .andThenTry( functionTakingCodbaleStructAndThrowing )
    // if any did thow an error then just recover with this one
    .defaultSome( CodableStruct.validInstance )

你可以在不同的尝试后以不同的方式 *恢复*。或者你可以完全忽略它。无论哪种方式,你都有一个不错的 API。

但是等等,还有更多!

有时你正在使用一个 Optional 集合。最常见的情况是 String 和一些东西的 Optional 数组。这个 Optional API 也为你考虑到了!

在下面的示例中,我将使用这些 Optionals。

let noneString     : String? = .none
let emptySomeString: String? = ""
let someSomeString : String? = "some string"

let noneIntArray : [Int]? = .none
let emptyIntArray: [Int]? = []
let someIntArray : [Int]? = [11, 22, 33]

我认为这应该涵盖所有情况。

Optional 集合有值、为 nil 或为空

当在 Optional 上下文中使用集合时,会产生很多 if 语句。这些属性应该有所帮助。

hasElements

noneString.hasElements      // false
emptySomeString.hasElements // false
someSomeString.hasElements  // true

noneIntArray.hasElements  // false
emptyIntArray.hasElements // false
someIntArray.hasElements  // true

isNoneOrEmpty

noneString.isNoneOrEmpty      // true
emptySomeString.isNoneOrEmpty // true
someSomeString.isNoneOrEmpty  // false

noneIntArray.isNoneOrEmpty  // true
emptyIntArray.isNoneOrEmpty // true
someIntArray.isNoneOrEmpty  // false

recoverFromEmpty

只有当底层集合为空时,才会调用它 only。也就是说,如果你的 optional 为 nil 或具有某些值,则不会调用它。由于 String 是一个集合,我将只展示 [Int]? 的示例 :)

noneIntArray.recoverFromEmpty([42])  // nil
emptyIntArray.recoverFromEmpty([42]) // [42]
someIntArray.recoverFromEmpty([42])  // [11, 22, 33]

如果你需要 none 情况的默认值,那么 defaultSome 就是你想要的。

noneIntArray.defaultSome([42])  // [42]
emptyIntArray.defaultSome([42]) // []
someIntArray.defaultSome([42])  // [11, 22, 33]

or

有些情况下,你需要从 Optional 获取一个实际结果 or 一个默认的非 optional 值。这正是 or 的情况。

let noneInt: Int? = .none
let someInt: Int? = .some(42)

var result: Int = someInt.or(69) // 42

在这种情况下,result 变量存储值 42。这是一个真正的 Int,而不是 optional。但如果它是 none 会发生什么呢?

result = noneInt.or(69) // 69

在这里,*最终* 结果是 69,因为一切都评估为 none。再次,在 or 之后,你有一个真实的值或一些默认值。

使用 or 的默认值

如果包装类型有一个空初始化器(不带参数的 init),你可以调用它来获取一个实例。

someOptional
    .or(.init()) // creates an instance

为了把它放在上下文中,如果你有一些 optionals,你可以使用它来获取 *零* 值,就像这样:

let noneInt: Int? = nil
noneInt.or( .init() ) // 0
noneInt.or( .zero   ) // 0

let noneDouble: Double? = nil
noneDouble.or( .init() ) // 0

let defaults: UserDefaults? = nil
defaults.or( .standard ) // custom or "standard"

let view: UIView? = nil
view.or( .init() )

// or any other init ;)
view.or( .init(frame: .zero) )

// Collections
let noneIntArray : [Int]? = .none
noneIntArray.or( .init() ) // []

let emptySomeString: String? = ""
noneString.or( .init() ) // ""

// Enums
enum Either {
    case left, right
}
let noneEither: Either? = nil
noneEither.or(.right)

你可以在此类型上调用的任何内容(静态方法)都可以在这里使用。

cast

你有没有写过类似这样的代码?

if let customVC = mysteryVC as? CustomVC {
    // do stuff
}

使用 cast,你可以将你的代码简化为这样:

 let someViewController: UIViewController? = ...
 someViewController
     .cast( CustomVC.self )
     .andThen({ (vc: CustomVC) in
        // work with a non optional instance of CustomVC
     })

如果可以从上下文中推断出类型,那么你就不必输入它。

let anyString: Any? = ...

let result: String? = anyString.cast()

正如你所见,编译器能够推断出正确的类型。但请注意,在更复杂的情况下,这可能会减慢你的编译速度。

如果你想获得更快的编译速度,那么始终显式声明你的类型。在你所有的代码中,而不仅仅是在使用这个包时。

encode & decode

当你想编码或解码某些内容时,常见的场景之一是你从网络获取了一些数据。流程可能如下所示:

为了保持简单,假设我们的数据传输模型 (DTO) 如下所示:

struct CodableStruct: Codable, Equatable {
    let number: Int
    let message: String
}

发生的情况是,一个 JSON 字符串通过网络作为数据发送。为了在代码中模拟这一点,可以这样写:

let codableStructAsData: Data? =
    """
    {
        "number": 55,
        "message": "data message"
    }
    """.data(using: .utf8)

舞台已就绪

decode

网络代码将递给我们一个 Data? 的实例,我们想要解码它。

let result: CodableStruct? = codableStructAsData.decode()

就这么简单。编译器可以推断出类型,所以没有必要显式添加它。但是你可以在一些更长的管道中这样做,例如:

codableStructAsData
    .decode( CodableStruct.self )
    .andThen({ instance in
        // work with not optional instance
    })

encode

编码是另一种方式。你有一个想要编码的实例,以便将其作为 json 发送。

let codableStruct: CodableStruct? =
    CodableStruct(
        number: 69,
        message: "codable message"
    )

要获得所需的编码值,只需使用该方法。

codableStruct
    .encode() // <- encoding part if you missed it ;)
    .andThen({ instance in
        // work with not optional instance
    })

whenSomewhenNone

当使用 optionals 时,有时 **你想要运行一些代码,但不更改 optional**。这就是可以使用 whenSomewhenNone 的地方。

let life: Int? = 42

life
    .whenSome { value in
        print("Value of life is:", value)
    }

此代码打印到控制台:*生命的价值是:42*。

whenSome 也有一种不需要参数的形式。

let life: Int? = 42

life
    .whenSome {
        print("Life is a mistery. But I know it's there!")
    }

这是一种非常好的触发某些逻辑的方式,而无需编写 if 语句。但是,当 optional 为 none 时(或者它被称为 nil 时)呢?

whenNone 在这里是为了救援。

    let life: Int? = .none

    life
        .whenNone {
            print("No life here!")
        }

*这里没有生命!* 将会被打印在控制台中。

但更酷的是,你可以链式调用它们!

let life: Int? = 42

life
    .whenSome { value in
        print("Value of life is:", value)
    }
    .whenSome {
        print("Life is a mistery. But I know it's there!")
    }
    .whenNone {
        print("No life here!")
    }

根据运算符和 optional 的值,将调用不同的代码块。当然,也可以混合使用其他运算符。

filter

有时你只需要在值通过某些谓词时才需要它。

let arrayWithTwoElements: [Int]? = [42, 69]

arrayWithTwoElements
    .filter { array in array.count > 1 }
    .andThen { ... } // work with array

还有这个运算符的免费版本。

filter<W>(_ predicate: @escaping (W) -> Bool ) -> (W?) -> W?

使用它来创建具有内置谓词的过滤器函数。

Async/Await

使用新的异步处理 API,你可以编写使用异步函数的代码。

// we are in asynchronous context

let someInt: Int? = 42

let result: Int? = await someInt
    .asyncFlatMap {
        try! await Task.sleep(nanoseconds: 42)
        return $0 + 1
    }
    .flatMap { fromAsync in
        fromAsync * 10
    }

正如你所见,将同步代码与异步代码混合使用很容易。请记住,await 必须位于管道的开头。如果你不这样做,那么你将收到来自编译器的友好提醒。

tryAsyncMap & tryAsyncFlatMap

tryAsyncMaptryAsyncFlatMap 是允许你在 Swift 中对 optional 值执行异步转换的方法。它们接受一个闭包,该闭包对 optional 值执行异步操作,并返回不同类型的 optional 值。

用法

这是一个如何使用 tryAsyncMap 的示例。

enum MyError: Error {
    case invalidInput
}

func doAsyncTransformation(value: Int?) async throws -> String {
    guard let value = value else {
        throw MyError.invalidInput
    }

    await Task.sleep(1_000_000_000) // Simulate long-running task.

    return "Transformed value: \(value)"
}

let optionalValue: Int? = 42

do {
    let transformedValue = try await optionalValue.tryAsyncMap { value in
        try doAsyncTransformation(value: value)
    }

    print(transformedValue) // Prints "Transformed value: 42".
} catch {
    print(error)
}

zip -- 已移动

此功能已移动到 Zippy 🤐 Swift Package。它具有针对不仅仅是 optionals 的更多类型的 zip 函数的定义。

🐇🕳 Rabbit Hole

此项目是 🐇🕳 Rabbit Hole Packages Collection 的一部分。

就这样

希望它能帮助你 :)

欢呼!:D