swift 结合声明式语法

bha*_*tsb 3 variadic swift combine

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

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

[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)")
})
Run Code Online (Sandbox Code Playgroud)

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

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

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

Ale*_*ica 5

首先清理代码

格式化

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

[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)")
        }
    )
Run Code Online (Sandbox Code Playgroud)

清理map表达式

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

  1. 使用隐式返回

    map({ (val) in
        return val * 3
    })
    
    Run Code Online (Sandbox Code Playgroud)
  2. 使用隐式返回

    map({ (val) in
        val * 3
    })
    
    Run Code Online (Sandbox Code Playgroud)
  3. 删除 param 声明周围不必要的括号

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

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

    map({ $0 * 3 })
    
    Run Code Online (Sandbox Code Playgroud)
  6. 使用尾随闭包语法

    map { $0 * 3 }
    
    Run Code Online (Sandbox Code Playgroud)

最后结果

带有编号的行,所以我可以很容易地参考。

/*  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 */    )
Run Code Online (Sandbox Code Playgroud)

通过它。

1号线, [1, 2, 3]

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

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

2号线, .publisher

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

它是一个Publisherwho Outputis Int,其Failure类型是Never(即不可能出现错误,因为无法创建该Never类型的实例)。

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:

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

最终发布者将完成,调用receiveCompletion:闭包。到的参数completionPARAM将类型的值Subscribers.Completion,这与无论是枚举.failure(Failure)的情况下,或.finished情况。由于Failure类型是Never,因此实际上不可能在.failure(Never)这里创建值。所以完成将始终是.finished,这将导致print("Received Completion")被调用。该语句print("Something went wrong: \(error)")是死代码,永远无法到达。

关于“声明”的讨论

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

在声明式风格中,您的程序由一系列声明组成。实现这些声明所必需的细节被抽象出来,例如像Combine和这样的库SwiftUI。例如,在这种情况下,您声明了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)
}
Run Code Online (Sandbox Code Playgroud)

但是有很多问题:

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

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

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

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

这段代码可以写成对 的调用map

let result = input.map(transform)
Run Code Online (Sandbox Code Playgroud)

这有很多好处:

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

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