【问题标题】:swift combine declarative syntax快速组合声明式语法
【发布时间】:2020-05-03 21:43:34
【问题描述】:

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(...)的一个参数.

【问题讨论】:

  • 您能否详细说明您对“声明式”语法的理解到底是什么?我认为这是混乱的根源
  • 我认为可以通过谷歌搜索“declarative syntax swift”找到。前三个结果。我编辑了我的问题,以澄清混淆之处。

标签: swift variadic combine


【解决方案1】:

文字

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

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

链接

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

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

写的更好

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

或者为了更清晰

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

结论

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

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

所以从句法的角度来看,没有什么是“不可见的”,除了知道每个方法调用返回一个可以应用下一个方法调用的值(就像我自己上面的示例代码一样,我将.count 应用于.filter 的结果)。当然,由于您的示例是 Combine,something 是“正在进行的不可见的”,即这些值中的每一个都是发布者、操作员或订阅者(订阅者确实订阅了)。但这基本上只是知道Combine是什么的问题。所以:

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

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

  • map 方法(Combine 的 map,而不是 Array 的 map)可以应用于发布者,并生成另一个发布者对象。

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

【讨论】:

  • 不清楚反对票的用途;我认为这清楚、完整、正确地回答了这个问题。
  • Idk 伙计,我得到了一些神秘的反对票。喜欢这个哈哈:stackoverflow.com/a/40779509/3141234
【解决方案2】:

先清理代码

格式化

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

[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. 使用隐式参数,而不是 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"truesomeVariable1 + 1。像这样的数组不需要分配给任何东西就可以使用。

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

第 2 行,.publisher

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

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

第 3 行,map

第 3 行正在调用上述 Combine.Publishers.Sequence<Array<Int>, Never> 值的 map 实例函数(又名方法)。每当一个元素通过这个链时,它都会被传递给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: 闭包。 completion 参数的参数将是 Subscribers.Completion 类型的值,它是具有 .failure(Failure) 大小写或 .finished 大小写的枚举。由于Failure类型是Never,所以这里实际上不可能创建.failure(Never)的值。所以完成总是.finished,这将导致print("Received Completion")被调用。语句print("Something went wrong: \(error)") 是死代码,永远无法到达。

关于“声明式”的讨论

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

在声明式风格中,您的程序由一系列声明组成。实现这些声明所需的细节被抽象出来,例如像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函数/闭包的输出数组。
  3. 它已经过优化,内部map 调用reserveCapacity,它永远不会忘记这样做。
  4. result 可以是不可变的。

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

【讨论】:

  • 我喜欢这个答案。它非常详细,但格式和解释都很好。不过,我确实认为将 map 表达式缩减为 map { $0 * 3 } 可能会让那些不完全适应声明式编程的人感到困惑。
  • @NRitH 这并不是真正的“适合声明式编程”的东西,它只是 Swift 中存在的一种非常常见/实用的语法。我总是反对不使用特定 Swift 功能的建议,因为非 Swift 开发人员不会理解它。这样做会将您带到一个共同点最低的领域,在此领域您限制自己使用现有的工具来轻松方便地解决您的问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-12-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多