通过在数组上减少与通过在迭代中分配每个项目来创建字典之间的区别

Difference between creating a dictionary by reduce on an array vs by assigning each item on iteration

我在 Whosebug 上遇到了一个问题:Swift - Convert Array to Dictionary 用户想要获取数组的元素并将它们放入字典并将 0 分配给它们中的每一个。 (关键是玩家,价值是他们的分数) 所以:

var playerNames = ["Harry", "Ron", "Hermione"]

变成

var scoreBoard: [String:Int] = [ "Ron":0, "Harry":0, "Hermione":0 ]

这个问题有2个答案: 1) 在数组上使用 reduce

let scoreboard = playerNames.reduce(into: [String: Int]()) { [=12=][] = 0 }

2) 创建一个字典并迭代数组以将每个值键对添加到它

var dictionary = [String: Int]()
for player in playerNames {
    dictionary[player] = 0
}

我使用了https://github.com/nyisztor/swift-algorithms中的BenchTimer函数来测试这两种解决方法。而且它们似乎都在 O(n) 中运行。

我想知道为什么我们更喜欢第一个而不是另一个,因为编写第二个解决方案的人对他们的编码技能发表了不好的评论。

编辑:有些功能在较新的版本中被 Apple 弃用了,所以坚持基本原则并创建我们自己的做事方式不是更好吗?

感谢您的回答

一般来说,您应该更喜欢函数式惯用语(即 reduce)而不是 for 循环,原因如下:

  1. 功能版本传达了意图。在这种情况下,我们是 将数组缩减为字典——这不是最好的例子,但是 通常通过一些 reduce 将向量转换为单个标量 组合函数。
  2. 功能版本经过测试默认正确。这在 reduce 的情况下可能看起来微不足道,但如果您查看 shuffled 之类的东西,它就不那么重要了。查看 for 循环并告诉我它是否执行 Fisher Yates 洗牌以及该洗牌是否正确实施有多容易?类似地,函数流水线比一堆连续的 for 循环或做 5 种不同事情的单个 for 循环更容易阅读。

  3. 功能版本通常(但在这种情况下不是)不可变的,不可变的值比可变状态更容易推理,因为它们永远不会改变。

  4. Swift.Sequence 上的功能方法大多在 Combine.Publisher 上有类似的方法。这意味着您可以在所有序列中使用一组功能习语,无论它们是同步的还是 asynchronous/reactive。

今天,IMO,你不应该使用其中任何一个。我们现在有 Dictionary.init(uniqueKeysWithValues:).init(_:uniquingKeysWith:),它们更清楚地说明了他们的意图,并使重复键等极端情况变得明确。

如果你可以静态地证明所有的键都是唯一的,那么你会使用第一个:

let scoreboard = Dictionary(uniqueKeysWithValues: playerNames.map { (name: [=10=], score: 0) })

如果您不能证明密钥是唯一的,您可以使用第二个密钥,它允许您明确决定在发生冲突时要做什么。

let scoreboard = Dictionary(playerNames.map { (name: [=11=], score: 0) },
                            uniquingKeysWith: { first, _ in first })

请注意这种方法如何让标签清楚地表明键是什么以及值是什么。我没有对这段代码进行基准测试,但我希望它在时间上与其他代码非常相似。

so isn't it better to stick with the basics and create our own ways to do things?

我不这么认为。 Swift 社区当然不是那种心态。 Swift 优先考虑进行有意义的抽象和简化,只要它们有价值,就可以逐步公开。

Go 社区分享您的思路,但它非常痛苦(IMO)。 Go 标准库甚至没有用于反转字符串的 API。你必须自己做饭。而且它比大多数人想象的要难。如果您认为制作一个循环来反转字节是一件简单的事情,不,这对于 unicode 来说是完全错误的(但可能会被简单的 ASCII 测试用例忽视,例如 "hello" 或其他)。

同样,如果每次要执行map时都写for循环,您可能会忘记调用Array.reserveCapacity(_:)。忘记这一点会导致多个数组分配,并使您的看起来 O(n) 的算法实际上变成了 O(n^2)。像这样的性能或正确性很少 "gotchas",因此使用这些东西的流行共享实现有很大好处。

我们站在巨人的肩膀上。如果我们全神贯注于重新发明轮子,我们就无法做到这一点。

关于这两段代码

我不会使用它们中的任何一个

第一种方法:

  1. 使用了一项功能 (reduce),但不适合工作 (Dictionary.init(uniqueKeysWithValues:))。参见 https://github.com/amomchilov/Blog/blob/master/Don'%20abuse%20reduce.md
  2. 忘记在字典上预留容量,所以导致多次调整大小操作(涉及内存重新分配,以及字典中所有键的重新散列)

第二种方法:

  1. 使字典不必要地可变
  2. 还忘记预留容量

相反,我会推荐 or Martin R's approach。它们都更能表达您的意图。 Rob 还使用了一个已知大小的序列,这允许 Dictionary.init(uniqueKeysWithValues:) 在内部预先为字典分配足够的内存,因为这将是适应所有值所必需的。