避免在Swift中连续使用"if let"声明

18

在Swift中,我使用if let语句来检查我的对象是否不是nil

if let obj = optionalObj
{
}

但有时候,我不得不面对连续的 if let 声明。

if let obj = optionalObj
{
    if let a = obj.a
    {
        if let b = a.b
        {
            // do stuff
        }
    }
}

我正在寻找一种避免连续使用 if let 声明的方法。

我会尝试这样做:

if let obj = optionalObj && if let a = obj.a && if let b = a.b
{
    // do stuff
}

但是 Swift 编译器不允许这样做。

有什么建议吗?


我一直在思考同样的问题。我有一个JSON字典,需要从中获取5个键。目前,我有一个嵌套的5级深度的if let块,但我不太喜欢它。 - Craig Otis
1
这里有一个类似的问题:https://dev59.com/VGAf5IYBdhLWcg3weyzZ。 - Martin R
@TheParamagneticCroissant:你说得对,我删除了之前的评论。 - Jean Lebrument
4个回答

27

更新

在Swift 1.2中,您可以执行以下操作:

if let a = optA, let b = optB {
  doStuff(a, b)
}

原始回答

在你的特定情况下,你可以使用可选链:


if let b = optionaObj?.a?.b {
  // do stuff
}

现在,如果您需要做的是:

if let a = optA {
  if let b = optB {
    doStuff(a, b)
  }
}

你没那么幸运,因为你无法使用可选链。

简短易懂版

你想要一句简洁的表达吗?

doStuff <^> optA <*> optB

请继续阅读。尽管看起来很可怕,但这实际上是非常强大的,并且使用起来并不像看起来那么疯狂。

幸运的是,这是一个可以轻松解决的问题,只需使用函数式编程方法即可。 您可以使用 Applicative 抽象,并提供一个apply方法来组合多个选项。

这里有一个例子,摘自http://robots.thoughtbot.com/functional-swift-for-dealing-with-optional-values

首先,我们需要一个函数,仅当可选值包含某些内容时才将函数应用于可选值

// this function is usually called fmap, and it's represented by a <$> operator
// in many functional languages, but <$> is not allowed by swift syntax, so we'll
// use <^> instead
infix operator <^> { associativity left }

func <^><A, B>(f: A -> B, a: A?) -> B? {
    switch a {
    case .Some(let x): return f(x)
    case .None: return .None
    }
}

然后我们可以使用 apply 将多个选项组合在一起,我们将其称为 <*>,因为我们很酷(而且我们懂一些 Haskell)

// <*> is the commonly-accepted symbol for apply
infix operator <*> { associativity left }

func <*><A, B>(f: (A -> B)?, a: A?) -> B? {
    switch f {
    case .Some(let value): return value <^> a
    case .None: return .None
    }
}

现在我们可以重新编写我们的例子。

doStuff <^> optA <*> optB

只要doStuff是柯里化形式的(见下文),这将起作用,即:

func doStuff(a: A)(b: B) -> C { ... }
整个事情的结果是一个可选值,可以是nil,也可以是doStuff的结果。

以下是在playground中尝试的完整示例:

func sum(a: Int)(b: Int) -> Int { return a + b }

let optA: Int? = 1
let optB: Int? = nil
let optC: Int? = 2

sum <^> optA <*> optB // nil
sum <^> optA <*> optC // Some 3

作为最后的说明,将函数转换为其柯里化形式非常简单。例如,如果您有一个接受两个参数的函数:

func curry<A, B, C>(f: (A, B) -> C) -> A -> B -> C {
    return { a in { b in f(a,b) } }
}

现在您可以使用柯里化(curry)任何两个参数的函数,比如说+

curry(+) <^> optA <*> optC // Some 3

4
这个答案可能看起来有点复杂,但在我看来,这是正确思考解决方案的方式。我们不应该只是将我们的 Objective-C 代码进行逐行移植。 - Abizern
2
@BradLarson oh yes,单子绑定绝对是一种有效的方法,我在其他语言中广泛使用它。但是我有两个担忧:单子比应用更强大,我通常倾向于选择最不强大的抽象;其次,Swift缺乏Haskell提供的do-blocks或Scala提供的for-blocks等语法糖,因此与应用相比使用起来可能更加麻烦。但两种方法都是有效的 :) - Gabriele Petronella
2
@JeanLebrument 这并不是我的观点,这就是我在上面评论中想表达的。语法支持还没有准备好进行单子绑定。 - Gabriele Petronella
@GabrielePetronella:好的,谢谢你的解释! - Jean Lebrument
美丽的。fmap。天哪。谢谢你。 - Benjamin
显示剩余7条评论

8
我之前写过一篇小论文,讨论了一些备选方案:https://gist.github.com/pyrtsa/77978129090f6114e9fb 其他回答中尚未提到的一种方法是,添加一堆重载的every函数,我有点喜欢这种方法。
func every<A, B>(a: A?, b: B?) -> (A, B)? {
    switch (a, b) {
        case let (.Some(a), .Some(b)): return .Some((a, b))
        default:                       return .None
    }
}

func every<A, B, C>(a: A?, b: B?, c: C?) -> (A, B, C)? {
    switch (a, b, c) {
        case let (.Some(a), .Some(b), .Some(c)): return .Some((a, b, c))
        default:                                 return .None
    }
}

// and so on...

这些可以用在if let语句、case表达式以及 optional.map(...)链中:

// 1.
var foo: Foo?
if let (name, phone) = every(parsedName, parsedPhone) {
    foo = ...
}

// 2.
switch every(parsedName, parsedPhone) {
    case let (name, phone): foo = ...
    default: foo = nil
}

// 3.
foo = every(parsedName, parsedPhone).map{name, phone in ...}

为了添加每个every的重载,会很繁琐但只需要在库中完成一次。同样地,使用应用函子方法(即使用<^><*>操作符)时,您需要以某种方式创建柯里化函数,这也会在某些地方导致一些样板代码。


1
有没有一种方法可以创建一个带有可变参数的every方法? - Jean Lebrument
是的,当然!非常感谢! - Jean Lebrument
2
值得注意的是,您可以从一对形成运算符和Gabrielle提到的应用程序中派生出每个东西。every2liftA2(,)every3liftA3(,,,) - CodaFi
2
正如CodaFi所提到的,如果你愿意,every实现可以建立在我的答案之上。一旦我们有了Applicative机制,剩下的就只是巧妙的语法了 ;) - Gabriele Petronella
1
这已经过度了,因为Swift 1.2。 - Wilfredo Sánchez Vega
显示剩余8条评论

2
在某些情况下,您可以使用可选链。对于您的简单示例:
在一些情况下,你可以使用可选链。对于你的简单示例:
if let b = optionalObj?.a?.b {
    // do stuff
}

为了减少嵌套并给自己相同的变量赋值,您也可以这样做:
if optionalObj?.a?.b != nil {
    let obj = optionalObj!
    let a = obj.a!
    let b = a.b!
}

是的,我考虑过这个问题,但在我的情况下,我需要每个对象的引用。 - Jean Lebrument

1

在听了Martin R的一些讲座之后,我发现了一个有趣的解决方法:https://dev59.com/iGAf5IYBdhLWcg3wgzHI#26012746

func unwrap<T, U>(a:T?, b:U?, handler:((T, U) -> ())?) -> Bool {
    switch (a, b) {
    case let (.Some(a), .Some(b)):
        if handler != nil {
            handler!(a, b)
        }
        return true
    default:
        return false
    }
}

这个解决方案很有意思,但如果方法使用可变参数会更好。
我天真地开始创建这样的方法:
extension Array
{
    func find(includedElement: T -> Bool) -> Int?
    {
        for (idx, element) in enumerate(self)
        {
            if includedElement(element)
            {
                return idx
            }
        }

        return nil
    }
}

func unwrap<T>(handler:((T...) -> Void)?, a:T?...) -> Bool
{

    let b : [T!] = a.map { $0 ?? nil}

    if b.find({ $0 == nil }) == nil
    {
        handler(b)
    }
}

但是我在编译器中遇到了这个错误:无法将表达式的类型 '[T!]' 转换为类型 '((T...) -> Void)?' 有什么解决方法建议吗?

请将此作为单独的问题提出。 - Gregory Higley

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接