【问题标题】:Initialize dictionary from transforming an array通过转换数组初始化字典
【发布时间】:2017-05-29 14:48:29
【问题描述】:

有没有一种方法可以快速地从数组中以声明方式初始化字典? 我正在寻找这样的东西:

struct MyStruct {

   var key: Int
   var value: String
}

let array = [MyStruct(key: 0, value: "a"), MyStruct(key: 1, value: "b")]
let dict = array.someTransform { // Some arguments
   // Some transformation
}

所以dict 的类型是[Int: String]

注意:我不是在寻找包含forEach 的解决方案,因为从这个任务的角度来看,它只是for 循环的更复杂版本。

【问题讨论】:

  • 比较stackoverflow.com/questions/24116271/… 有类似的问题。从 Swift 3 开始,没有内置方法可以从键/值对的数组(或序列)创建字典。 Swift 4 可能会改变这种情况。
  • forEach 确实不是 for 循环的更复杂版本 - 事实上,它在许多不同的方面更少复杂(例如,不能模式匹配或where 子句,很难突破)。

标签: swift


【解决方案1】:

Dictionary 的序列初始化器

在 Swift 4 中,假设键保证是唯一的,你可以简单地说:

let array = [MyStruct(key: 0, value: "a"), MyStruct(key: 1, value: "b")]

let dict = Dictionary(uniqueKeysWithValues: array.lazy.map { ($0.key, $0.value) })

print(dict) // [0: "a", 1: "c"]

这是使用来自SE-0165init(uniqueKeysWithValues:) 初始化程序。它需要一个键值元组序列,其中的键保证是唯一的(如果不是,你会得到一个致命错误)。因此,在这种情况下,我们将对数组中的元素应用惰性转换,以获得键值对的惰性集合。

如果键保证是唯一的,您将需要某种方法来决定将哪些可能的值用于给定键。为此,您可以使用 init(_:uniquingKeysWith:) 初始化程序 from the same proposal,并传递给定函数以确定在出现重复键时为给定键使用哪个值。

uniquingKeysWith: 函数的第一个参数是字典中已经存在的值,第二个是试图插入的值。

例如,这里每次在序列中出现重复键时,我们都会覆盖该值:

let array = [MyStruct(key: 0, value: "a"), MyStruct(key: 0, value: "b"),
             MyStruct(key: 1, value: "c")]

let keyValues = array.lazy.map { ($0.key, $0.value) }
let dict = Dictionary(keyValues, uniquingKeysWith: { _, latest in latest })

print(dict) // [0: "b", 1: "c"]

要保留给定键的第一个值,并忽略同一键的任何后续值,您需要{ first, _ in first }uniquingKeysWith: 闭包,在这种情况下给出[0: "a", 1: "c"] 的结果。


使用inout 累加器减少

在 Swift 4 中,另一个可能的选项是使用 reduce(into:_:),在 SE-0171 中引入,假设您希望通过覆盖每次出现给定键的值来合并任何重复键。

reduce(_:_:) 不同,此方法使用inout 参数作为组合函数中的累加器。这允许它避免在填充字典累加器时在reduce(_:_:) 的每次迭代中发生不必要的累加器复制。因此,这允许我们以线性时间而不是二次时间来填充它。

你可以这样使用它:

let array = [MyStruct(key: 0, value: "a"), MyStruct(key: 0, value: "b"),
             MyStruct(key: 1, value: "c")]

let dict = array.reduce(into: [:]) { $0[$1.key] = $1.value }

print(dict) // [0: "b", 1: "c"]


// with initial capacity to avoid resizing upon populating.
let dict2 = array.reduce(into: Dictionary(minimumCapacity: array.count)) { dict, element in
    dict[element.key] = element.value
}

print(dict2) // [0: "b", 1: "c"]

【讨论】:

    【解决方案2】:

    您还可以扩展 Dictionary 类型以添加​​一个接受元组数组的初始化程序:

    extension Dictionary
    {
      init(_ keyValues:[(Key,Value)] )
      {
         self.init(minimumCapacity: keyValues.underestimatedCount) // self.init()
         for (key,value) in keyValues { self[key] = value }
      }
    }
    
    struct MyStruct {
    
       var key: Int
       var value: String
    }
    
    let array = [MyStruct(key: 0, value: "a"), MyStruct(key: 1, value: "b")]
    let dict = Dictionary(array.map{($0.key,$0.value)})
    

    for 循环仍然存在,但仅在 Dictionary 类型中,并且不需要样板代码在需要此类初始化的各个位置以数组形式构建它。

    [EDIT] 将 init 更改为使用 3441734 建议的最小容量。这应该使其与 #1 一样快。但是,我觉得这种优化牺牲了一点简单性,因为这种初始化将成为关键性能因素的非常罕见的用例。

    【讨论】:

    • 又好又干净!这是我的测试数据的“测试”结果 4 -3.83605802059174 10000000
    • 要像其他解决方案一样执行,请执行 self = Dictionary(minimumCapacity: keyValues.underestimatedCount)
    【解决方案3】:

    使用减少

    let dict = array.reduce([:]) { (d, s) -> [Int:String] in
        var d = d
        d[s.key] = s.value
        return d
    }
    

    正如 @Martin R 所提到的,这不是表现最好的,但非常易于使用。 @Hamish的扩展不错,至少同样的性能给你简单一点

    var dict:[Int:String] = [:]
    for s in array {
        dict[s.key] = s.value
    }
    

    是的,我明白了,您希望避免使用 forEach 版本,但实际上,它是一个很好且强大的解决方案。

    var dict:[Int:String] = [:]
    array.forEach {
        dict[$0.key] = $0.value
    }
    

    使事情尽可能简单,减少产生不良副作用(错误)的机会

    定义最小容量

    var dict = Dictionary<Int,String>(minimumCapacity: array.count)
    array.forEach {
        dict[$0.key] = $0.value
    }
    

    你的表现最好。

    比较解决方案

    do {
        let start = Date()
        let dict = Dictionary(uniqueKeysWithValues: array.lazy.map { ($0.key, $0.value) })
        let time = start.timeIntervalSince(Date())
        print(1,time, dict.count)
    }
    
    do {
        let start = Date()
        var dict = Dictionary<Int,String>(minimumCapacity: array.count)
        array.forEach {
            dict[$0.key] = $0.value
        }
        let time = start.timeIntervalSince(Date())
        print(2,time, dict.count)
    }
    

    它打印在我的电脑上

    1 -1.93269997835159 10000000
    2 -1.80712699890137 10000000
    

    我喜欢 @Hamish 使用 inout 参数 作为累加函数的想法。我用相同的数据集测试过

    do {
        let start = Date()
        let dict = array.reduce(into: Dictionary(minimumCapacity: array.count)) { dict, element in
            dict[element.key] = element.value
        }
        let time = start.timeIntervalSince(Date())
        print(3,time, dict.count)
    }
    

    我期望与上面其他的性能相同,但不幸的是,它打印出来了

    3 -3.80046594142914 10000000
    

    看起来它需要 cc 两次才能执行相同的工作。

    【讨论】:

    • 这实际上是效率最低的解决方案:为每次迭代创建临时字典。比较 stackoverflow.com/a/28502842/1187415 和以下 cmets,或本文:airspeedvelocity.net/2015/08/03/…
    • @MartinR 你绝对正确。对于与 Hamish 的解决方案相同的性能,我更喜欢更简单的解决方案。
    • 有趣的是,reduce(into:_:) 的执行速度较慢——我无法立即想到减速的确切原因在哪里。虽然,除非专门用于高性能应用程序,否则我认为 2 倍的差异对于大多数正常用例来说并没有那么糟糕。
    • @Hamish 绝对
    • @Hamish 最有可能的原因是因为 inout 不是按引用传递,它只是在函数退出时写回调用者的参数的可变影子副本
    猜你喜欢
    • 2019-06-12
    • 2018-10-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-11-20
    • 2014-09-23
    • 1970-01-01
    • 2014-07-29
    相关资源
    最近更新 更多