Swift Optional Monad 的可选扩展... 用不用... 它是可选的。
在 Swift 中使用 Optionals 时,会出现一些常见的习惯用法。这里有一些针对某些类型的有用扩展。
只需复制粘贴文件到你的项目 🍝
或者使用 SPM 😎
GitHub 页面: OptionalAPI
旧
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 的操作
func maybeIncrement(_ i: Int) -> Int? { i + 1 }
旧的糟糕方式
if let trueInt = someIntOptional {
let incrementedOnce = maybeIncrement(trueInt) {
// you get the idea ;)
}
}
someOptional
.andThen(maybeIncrement)
.andThen(maybeIncrement)
// ... you get the idea :)
在这种情况下,此链式调用的结果是 Int?
的实例。如果 someOptional
为 nil,则整个计算结果为 nil。如果它有一些值 (42),那么它将被递增这么多次。
假设你有一系列操作,并且结果可能返回 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)
如果 someOptional
以 10
开头,并且我们很幸运(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。
此运算符期望一个可能抛出错误的转换。当这种情况发生时,它返回 .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 上下文中使用集合时,会产生很多 if 语句。这些属性应该有所帮助。
noneString.hasElements // false
emptySomeString.hasElements // false
someSomeString.hasElements // true
noneIntArray.hasElements // false
emptyIntArray.hasElements // false
someIntArray.hasElements // true
noneString.isNoneOrEmpty // true
emptySomeString.isNoneOrEmpty // true
someSomeString.isNoneOrEmpty // false
noneIntArray.isNoneOrEmpty // true
emptyIntArray.isNoneOrEmpty // true
someIntArray.isNoneOrEmpty // false
只有当底层集合为空时,才会调用它 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]
有些情况下,你需要从 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
之后,你有一个真实的值或一些默认值。
如果包装类型有一个空初始化器(不带参数的 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)
你可以在此类型上调用的任何内容(静态方法)都可以在这里使用。
你有没有写过类似这样的代码?
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()
正如你所见,编译器能够推断出正确的类型。但请注意,在更复杂的情况下,这可能会减慢你的编译速度。
如果你想获得更快的编译速度,那么始终显式声明你的类型。在你所有的代码中,而不仅仅是在使用这个包时。
当你想编码或解码某些内容时,常见的场景之一是你从网络获取了一些数据。流程可能如下所示:
为了保持简单,假设我们的数据传输模型 (DTO) 如下所示:
struct CodableStruct: Codable, Equatable {
let number: Int
let message: String
}
发生的情况是,一个 JSON 字符串通过网络作为数据发送。为了在代码中模拟这一点,可以这样写:
let codableStructAsData: Data? =
"""
{
"number": 55,
"message": "data message"
}
""".data(using: .utf8)
舞台已就绪
网络代码将递给我们一个 Data?
的实例,我们想要解码它。
let result: CodableStruct? = codableStructAsData.decode()
就这么简单。编译器可以推断出类型,所以没有必要显式添加它。但是你可以在一些更长的管道中这样做,例如:
codableStructAsData
.decode( CodableStruct.self )
.andThen({ instance in
// work with not optional instance
})
编码是另一种方式。你有一个想要编码的实例,以便将其作为 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
})
当使用 optionals 时,有时 **你想要运行一些代码,但不更改 optional**。这就是可以使用 whenSome
和 whenNone
的地方。
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 的值,将调用不同的代码块。当然,也可以混合使用其他运算符。
有时你只需要在值通过某些谓词时才需要它。
let arrayWithTwoElements: [Int]? = [42, 69]
arrayWithTwoElements
.filter { array in array.count > 1 }
.andThen { ... } // work with array
还有这个运算符的免费版本。
filter<W>(_ predicate: @escaping (W) -> Bool ) -> (W?) -> W?
使用它来创建具有内置谓词的过滤器函数。
使用新的异步处理 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
是允许你在 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)
}
此功能已移动到 Zippy 🤐 Swift Package。它具有针对不仅仅是 optionals 的更多类型的 zip
函数的定义。
此项目是 🐇🕳 Rabbit Hole Packages Collection 的一部分。
希望它能帮助你 :)
欢呼!:D