swift 组合声明语法

swift combine declarative syntax

SwiftCombine 的声明语法对我来说看起来很奇怪,而且似乎有很多不可见的事情正在发生。

例如,以下代码示例在 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] 创建的数组文字实例。我想它是一个数组文字,但我不习惯看到它 "declared" 而不是将它分配给变量名或常量或使用 _=.

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

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

文字

首先,关于文字。您可以在任何可以使用包含相同值的变量的地方使用文字。

之间没有重要区别
let arr = [1,2,3]
let c = arr.count

let c = [1,2,3].count

空格

其次,关于空格。简而言之,Swift 不关心您是否在点之前拆分语句。所以

没有区别
let c = [1,2,3].count

let c = [1,2,3]
    .count

链接

当您一个接一个地链接 lot 函数时,拆分实际上是提高易读性的好方法。而不是

let c = [1,2,3].filter {[=14=]>2}.count

写的更好看

let c = [1,2,3]
    .filter {[=15=]>2}
    .count

或者为了更清楚

let c = [1,2,3]
    .filter {
        [=16=]>2
    }
    .count

结论

这就是您显示的代码中发生的所有事情:文字后跟一长串方法调用。为了便于阅读,它们被分成不同的行,仅此而已。

所以你在问题中提到的任何内容都与 Combine 无关。这只是关于 Swift 语言的基本内容。您所谈论的一切都可能(并且确实)发生在根本不使用 Combine 的代码中。

所以从语法的角度来看,没有什么是"going on that is not visible",除了知道每个方法调用returns一个值,下一个方法调用可以应用(就像我自己的上面的示例代码,其中我将 .count 应用于 .filter 的结果)。当然,由于您的示例是 Combine,因此 something 是 "going on that is not visible",即这些值中的每一个都是发布者、运营商或订阅者(并且订阅者确实订阅了).但这基本上只是了解什么是 Combine 的问题。所以:

  • [1,2,3]是一个数组,它是一个序列,所以它有一个publisher方法。

  • publisher方法,可应用于序列,产生发布者。

  • map方法(Combine的map,不是Array的map)可以应用于一个publisher,产生另一个对象,它是一个publisher。

  • sink方法可以应用于它,并产生一个订阅者,这就是链的末端。

先清理代码

正在格式化

首先,reading/understanding如果格式正确,这段代码会容易得多。那么让我们从这里开始:

[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. 使用隐式 return

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

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

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

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

    map({ [=15=] * 3 })
    
  6. 使用尾随闭包语法

    map { [=16=] * 3 }
    

最终结果

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

/*  1 */[1, 2, 3]
/*  2 */    .publisher
/*  3 */    .map { [=17=] * 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"truesomeVariable1 + 1。像这样的数组不需要分配给任何东西就可以使用。

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

第 2 行,.publisher

第 2 行调用数组文字的 publisher 属性。这 return 是 Sequence<Array<Int>, Never>。这不是常规 Swift.Sequence, which is a non-generic protocol, but rather, it's found in the Publishers namespace (a case-less enum) of the Combine module. So its fully qualified type is Combine.Publishers.Sequence<Array<Int>, Never>.

这是一个 Publisher,其 OutputInt,其 Failure 类型为 Never(即不可能出现错误,因为有无法创建 Never 类型的实例)。

第 3 行,map

第 3 行正在调用上面 Combine.Publishers.Sequence<Array<Int>, Never> 值的 map 实例函数(a.k.a. 方法)。每次一个元素通过这个链时,它都会被给定的闭包转换 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. 的参数
  3. { value in ... } 闭包作为参数提供给标记为 receiveValue:
  4. 的参数

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

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

关于"declarative"的讨论

没有单一的句法元素使此代码符合 "declarative" 的条件。声明式风格与 "imperative" 风格不同。在命令式风格中,您的程序由一系列命令或要完成的步骤组成,通常具有非常严格的顺序。

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

我最喜欢的伪装命令式和声明式样式的示例是使用类似 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

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

  4. result 数组必须是可变的,即使您可能永远不需要在构建后改变它。

此代码可以改为调用 map:

let result = input.map(transform)

这有很多好处:

  1. 它更短(虽然并不总是一件好事,在这种情况下,让它更短也没什么损失)
  2. 更清楚了。 map 是一个非常特殊的工具,它只能做一件事。一看到 map,就知道 input.count == result.count,结果是 transform function/closure.
  3. 的输出数组
  4. 已优化,内部map调用reserveCapacity,永远不会忘记这样做。
  5. result可以是不可变的。

调用 map 遵循更具声明性的编程风格。您不会摆弄数组大小、迭代、追加或其他任何细节。如果你有 input.map { [=103=] * [=103=] },你就是说 "I want the input's elements squared",结束。 map 的实现需要 for 循环、appends 等。虽然它是以命令式风格实现的,但该函数将其抽象化,并允许您在更高的抽象级别上编写代码,而不必处理 for 循环等不相关的事情。