快速组合声明式语法

时间:2020-01-17 01:13:06

标签: swift variadic combine

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

2 个答案:

答案 0 :(得分:3)

先清理代码

格式化

开始时,如果格式正确,则阅读/理解此代码会容易得多。让我们开始吧:

[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],然后您将得到一个包含Set12的{​​{1}}。在没有其他类型信息的情况下(例如,上面的3类型注释),S​​wift使用Set作为首选的数组文字类型。

第2行,Array

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

这是一个Combine,其PublisherOutput,其Int类型为Failure(即,不可能发生错误,因为没有创建Never类型的实例的方法)。

第3行,Never

第3行正在调用上述map值的map实例函数(又称方法)。每当元素通过该链传递时,都会通过给定Combine.Publishers.Sequence<Array<Int>, Never>的闭包来对其进行转换。

  • map进入,1出来。
  • 然后3进入,2出来。
  • 最后6进入,3出来。

到目前为止,该表达式的结果是另一个6

第4行,Combine.Publishers.Sequence<Array<Int>, Never>

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

  1. sink(receiveCompletion:receiveValue:)闭包作为参数为{ completion in ... }的参数的自变量
  2. receiveCompletion:闭包作为参数为{ value in ... }的参数的自变量

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

最终,发布者将完成,调用value关闭。 receiveCompletion:参数的参数将是类型Subscribers.Completion的值,该值是具有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(_:)

但是有很多问题:

  1. 很多样板代码最终都会在整个代码库中重复,只有微小的差别。
  2. 阅读起来比较棘手。由于var input: [InputType] = ... var result = [ResultType]() for element in input { let transformedElement = transform(element) result.append(result) } 是这样的一般构造,因此在这里有许多可能。为了确切地了解发生了什么,您需要详细研究。
  3. 您没有致电for,因此错过了优化机会。对Array.reserveCapacity(_:)的这些重复调用可以达到append数组缓冲区的最大容量。那时:

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

    这些操作可能会变得昂贵。并且,随着添加的元素越来越多,您可能会几次用尽容量,从而导致许多此类重新生成操作。通过调用transformedElement,您可以告诉数组预先分配一个大小合适的缓冲区,这样就不需要进行增长操作了。

  4. result.reserveCapacity(input.count)数组必须是可变的,即使您可能不需要在构造后对其进行突变。

该代码可以写为对result的调用:

map

这有很多好处:

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

调用result遵循一种更具声明性的编程风格。您无需摆弄数组大小,迭代,附加等内容。如果您有map,则说的是“我希望输入的元素平方”。 map的实现将需要执行input.map { $0 * $0 }循环和for等操作。虽然它以命令式实现,但该函数将其抽象化,并允许您以更高的抽象级别编写代码,而不必在乎诸如append循环之类的无关紧要的东西。

答案 1 :(得分:2)

文学

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

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,因此某事是“不可见的”,也就是说,这些值中的每一个都是发布者,运算符或订阅者(而这些订阅者实际上是在订阅)。但这基本上只是知道什么是Combine。所以:

  • [1,2,3]是一个序列数组,因此具有publisher方法。

  • 可以应用于序列的publisher方法产生一个发布者。

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

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