首页 > 解决方案 > 快速组合声明式语法

问题描述

Swift Combine 的声明式语法对我来说看起来很奇怪,而且似乎有很多事情是不可见的。

例如,以下代码示例在 Xcode 游乐场中构建和运行:

[1, 2, 3]

.publisher
.map({ (val) in
        return val * 3
    })

.sink(receiveCompletion: { completion in
  switch completion {
  case .failure(let error):
    print("Something went wrong: \(error)")
  case .finished:
    print("Received Completion")
  }
}, receiveValue: { value in
  print("Received value \(value)")
})

我看到我假设是用 [1, 2, 3] 创建的数组文字实例。我猜它是一个数组文字,但我不习惯看到它“声明”而不将它分配给变量名或常量或使用_=。

我故意在 .publisher 之后添加了一个新行。Xcode 是否忽略了空格和换行符?

由于这种风格,或者我对视觉解析这种风格的新奇,我错误地认为“,receiveValue:”是一个可变参数或一些新语法,但后来意识到它实际上是.sink(...)的一个参数。

标签: swiftvariadiccombine

解决方案


先清理代码

格式化

首先,如果格式正确,阅读/理解这段代码会容易得多。所以让我们从那个开始:

[1, 2, 3]
    .publisher
    .map({ (val) in
        return val * 3
    })
    .sink(
        receiveCompletion: { completion in
            switch completion {
            case .failure(let error):
                print("Something went wrong: \(error)")
            case .finished:
                print("Received Completion")
            }
        },
        receiveValue: { value in
            print("Received value \(value)")
        }
    )

清理map表达式

我们可以通过以下方式进一步清理地图:

  1. 使用隐式返回

    map({ (val) in
        return val * 3
    })
    
  2. 使用隐式返回

    map({ (val) in
        val * 3
    })
    
  3. 删除参数声明周围不必要的括号

    map({ val in
        val * 3
    })
    
  4. 删除不必要的换行符。有时它们对于视觉分离事物很有用,但这是一个足够简单的闭包,它只会增加不必要的噪音

    map({ val in val * 3 })
    
  5. 使用隐式参数,而不是 a val,无论如何它都是非描述性的

    map({ $0 * 3 })
    
  6. 使用尾随闭包语法

    map { $0 * 3 }
    

最后结果

带有编号的行,所以我可以轻松地参考。

/*  1 */[1, 2, 3]
/*  2 */    .publisher
/*  3 */    .map { $0 * 3 }
/*  4 */    .sink(
/*  5 */        receiveCompletion: { completion in
/*  6 */            switch completion {
/*  7 */            case .failure(let error):
/*  8 */                print("Something went wrong: \(error)")
/*  9 */            case .finished:
/* 10 */                print("Received Completion")
/* 11 */            }
/* 12 */        },
/* 13 */        receiveValue: { value in
/* 14 */            print("Received value \(value)")
/* 15 */        }
/* 16 */    )

穿过它。

1号线,[1, 2, 3]

第 1 行是一个数组文字。它是一个表达式,就像1, "hi", true, someVariableor一样1 + 1。像这样的数组不需要分配给任何东西就可以使用。

有趣的是,这并不一定意味着它是一个数组。相反,Swift 拥有ExpressibleByArrayLiteralProtocol. 任何符合标准的类型都可以从数组字面量中初始化。例如,Set符合,所以你可以写:let s: Set = [1, 2, 3],你会得到一个Set包含123。在没有其他类型信息的情况下(例如Set上面的类型注释),S​​wift 将Array其用作首选的数组字面量类型。

2号线,.publisher

第 2 行正在调用publisher数组文字的属性。这会返回一个Sequence<Array<Int>, Never>. 这不是常规的Swift.Sequence,它是非通用协议,而是在模块的Publishers命名空间(无大小写枚举)中找到Combine。所以它的完全限定类型是Combine.Publishers.Sequence<Array<Int>, Never>.

它是whois ,其Publisher类型是(即不可能出现错误,因为无法创建该类型的实例)。OutputIntFailureNeverNever

3号线,map

第 3 行正在调用上述值的map实例函数(又名方法)Combine.Publishers.Sequence<Array<Int>, Never>。每当一个元素通过这个链时,它都会被给定的闭包所转换map

  • 1会进去,3会出来。
  • 然后2会进去,然后6会出来。
  • 终于3进去了,又6出来了。

到目前为止,这个表达式的结果是另一个Combine.Publishers.Sequence<Array<Int>, Never>

4号线,sink(receiveCompletion:receiveValue:)

第 4 行是对 的调用Combine.Publishers.Sequence<Array<Int>, Never>.sink(receiveCompletion:receiveValue:)。有两个闭包参数。

  1. { completion in ... }闭包作为参数提供给标记为的参数receiveCompletion:
  2. { value in ... }闭包作为参数提供给标记为的参数receiveValue:

Subscription<Array<Int>, Never>Sink 正在为我们上面的价值创建一个新的订阅者。当元素通过时,receiveValue闭包将被调用,并作为参数传递给它的value参数。

最终发布者将完成,调用receiveCompletion:闭包。param的completion参数将是 type 的值Subscribers.Completion,它是一个带有.failure(Failure)case 或.finishedcase 的枚举。由于Failure类型是Never,实际上不可能在.failure(Never)这里创建值。所以完成将永远是.finished,这将导致print("Received Completion")被调用。该语句print("Something went wrong: \(error)")是死代码,永远无法到达。

关于“声明式”的讨论

没有单一的句法元素可以使这段代码符合“声明性”的条件。声明式风格是与“命令式”风格的区别。在命令式风格中,您的程序由一系列命令式或要完成的步骤组成,通常具有非常严格的顺序。

在声明式风格中,您的程序由一系列声明组成。实现这些声明所必需的细节被抽象出来,例如像CombineSwiftUI. 例如,在这种情况下,您声明print("Received value \(value)")该数字是三倍的,只要有一个数字来自[1, 2, 3].publisher. 发布者是一个基本示例,但您可以想象一个发布者从文本字段发出值,其中事件发生在未知时间。

我最喜欢伪装命令式和声明式风格的例子是使用类似Array.map(_:).

可以写:

var input: [InputType] = ...
var result = [ResultType]()

for element in input {
    let transformedElement = transform(element)
    result.append(result)
}

但是有很多问题:

  1. 您最终会在整个代码库中重复大量样板代码,只有细微的差别。
  2. 阅读起来更棘手。既然for是这样一个普遍的结构,很多事情在这里都是可能的。要确切了解发生了什么,您需要查看更多细节。
  3. 您错过了优化机会,因为没有调用Array.reserveCapacity(_:). 这些重复调用append可以达到result数组缓冲区的最大容量。在那时候:

    • 必须分配一个新的更大的缓冲区
    • result需要复制的现有元素
    • 需要释放旧缓冲区
    • 最后,transformedElement必须添加新的

    这些操作可能会变得昂贵。随着您添加越来越多的元素,您可能会多次耗尽容量,从而导致多次此类重新增长操作。通过 callined result.reserveCapacity(input.count),您可以告诉数组预先分配一个大小合适的缓冲区,这样就不需要重新增长操作。

  4. 数组必须是可变的result,即使您可能不需要在构建后对其进行变异。

这段代码可以改为调用map

let result = input.map(transform)

这有很多好处:

  1. 它更短(虽然并不总是一件好事,在这种情况下,让它更短不会丢失任何东西)
  2. 更清楚了。map是一个非常具体的工具,它只能做一件事。只要您看到map,您就会知道input.count == result.count,并且结果是transform函数/闭包的输出数组。
  3. 它经过优化,内部map调用reserveCapacity,并且永远不会忘记这样做。
  4. result可以是不可变的。

调用map遵循更具声明性的编程风格。您不会摆弄数组大小、迭代、追加或其他任何细节。如果你有input.map { $0 * $0 },你说的是“我想要输入的元素平方”,结束。map 的实现将具有执行此操作所必需的for循环、appends 等。for虽然它是以命令式风格实现的,但该函数将其抽象化,并允许您在更高的抽象级别编写代码,而您不会在循环等不相关的事情上胡思乱想。


推荐阅读