在Swift中将对象数组映射到Dictionary


101

我有个Person的对象数组:

class Person {
   let name:String
   let position:Int
}

数组是:

let myArray = [p1,p1,p3]

我想映射myArray成为字典[position:name]的经典解决方案是:

var myDictionary =  [Int:String]()

for person in myArray {
    myDictionary[person.position] = person.name
}

有雨燕任何优雅的方式做到这一点与功能的办法mapflatMap......或其他现代风格的雨燕


游戏有点晚了,但是您想要的字典[position:name]还是[position:[name]]?如果您有两个人处于同一位置,则您的字典将仅保留循环中遇到的最后一个人。...我有一个类似的问题,我正在尝试寻找解决方案,但我希望结果像[1: [p1, p3], 2: [p2]]
Zonker.in.Geneva,

1
它们是内置功能。看这里。基本上是这样Dictionary(uniqueKeysWithValues: array.map{ ($0.key, $0) })
亲爱的,

Answers:


133

好的map不是一个很好的例子,因为它和循环一样,您可以使用reduce它,它将每个对象合并并变成单个值:

let myDictionary = myArray.reduce([Int: String]()) { (dict, person) -> [Int: String] in
    var dict = dict
    dict[person.position] = person.name
    return dict
}

//[2: "b", 3: "c", 1: "a"]

在Swift 4或更高版本中,请使用以下答案以获得更清晰的语法。


8
我喜欢这种方法,但是比仅创建一个循环来加载字典更有效吗?我担心性能,因为似乎每个项目都必须创建一个更大的字典的新可变副本...
Kendall Helmstetter Gelner

它实际上是一个循环,这种方式是简化的编写方式,其性能与正常循环相同或有所提高
Tj3n

2
Kendall,请参阅下面的答案,它可能比使用Swift 4时的循环速度更快。尽管如此,我尚未对其进行基准测试来确认这一点。
possen

3
不要使用这个!!!正如@KendallHelmstetterGelner指出的那样,这种方法的问题在于每次迭代,它都以当前状态复制整个字典,因此在第一个循环中,它是复制一个单项字典。第二次迭代是复制一个两个项目的字典。这是巨大的性能消耗!更好的方法是在字典上编写一个方便的init,该字典接受您的数组并在其中添加所有项。这样,对于消费者来说,它仍然是单线的,但是它消除了它所具有的整个分配混乱。另外,您可以基于数组进行预分配。
Mark A. Donohoe

2
我认为性能不会成为问题。Swift编译器将识别出字典的所有旧副本都不会被使用,甚至可能不会创建它们。记住优化自己的代码的第一条规则:“不要这样做”。计算机科学的另一个准则是,只有当N很大而N几乎永远不会很大时,有效的算法才有用。
bugloaf

149

由于Swift 4您可以使用它的into版本更干净有效地执行@ Tj3n的方法,reduce因此它消除了临时字典和返回值,因此它更快,更容易阅读。

示例代码设置:

struct Person { 
    let name: String
    let position: Int
}
let myArray = [Person(name:"h", position: 0), Person(name:"b", position:4), Person(name:"c", position:2)]

Into 参数传递结果类型为空的字典:

let myDict = myArray.reduce(into: [Int: String]()) {
    $0[$1.position] = $1.name
}

直接返回传入的类型的字典into

print(myDict) // [2: "c", 0: "h", 4: "b"]

我对为什么它似乎只与位置参数($ 0,$ 1)一起使用而不是在我命名它们时起作用感到困惑。编辑:没关系,因为我仍然尝试返回结果,所以编译器可能崩溃了。
Departamento B

这个答案中不清楚的是什么$0表示:它是inout我们正在“返回”的字典–尽管不是真正返回,因为对其进行修改相当于由于inout-ness而返回。
adamjansch

96

Swift 4开始,您可以轻松完成此操作。有两个 新的初始化程序根据元组序列(键和值对)构建字典。如果保证键是唯一的,则可以执行以下操作:

let persons = [Person(name: "Franz", position: 1),
               Person(name: "Heinz", position: 2),
               Person(name: "Hans", position: 3)]

Dictionary(uniqueKeysWithValues: persons.map { ($0.position, $0.name) })

=> [1: "Franz", 2: "Heinz", 3: "Hans"]

如果任何键重复,将失败并显示运行时错误。在这种情况下,您可以使用以下版本:

let persons = [Person(name: "Franz", position: 1),
               Person(name: "Heinz", position: 2),
               Person(name: "Hans", position: 1)]

Dictionary(persons.map { ($0.position, $0.name) }) { _, last in last }

=> [1: "Hans", 2: "Heinz"]

这相当于您的for循环。如果您不想“覆盖”值并坚持使用第一个映射,则可以使用以下方法:

Dictionary(persons.map { ($0.position, $0.name) }) { first, _ in first }

=> [1: "Franz", 2: "Heinz"]

Swift 4.2添加了第三个初始化器,该初始化器将序列元素分组为字典。字典键由闭包派生。具有相同键的元素以与序列中相同的顺序放入数组中。这使您可以获得与上述类似的结果。例如:

Dictionary(grouping: persons, by: { $0.position }).mapValues { $0.last! }

=> [1: Person(name: "Hans", position: 1), 2: Person(name: "Heinz", position: 2)]

Dictionary(grouping: persons, by: { $0.position }).mapValues { $0.first! }

=> [1: Person(name: "Franz", position: 1), 2: Person(name: "Heinz", position: 2)]


3
根据docs的事实,“分组”初始化器以数组作为值创建字典(这就是“分组”的意思,对吗?),在上述情况下,它更像[1: [Person(name: "Franz", position: 1)], 2: [Person(name: "Heinz", position: 2)], 3: [Person(name: "Hans", position: 3)]]。不是预期的结果,但是如果您愿意,可以将其进一步映射到平面字典。
Varrry

尤里卡!这是我一直在寻找的机器人!这是完美的,大大简化了我的代码。
Zonker.in.Geneva,

指向初始化程序的无效链接。您能否更新以使用永久链接?
Mark A. Donohoe

@MarqueIV我修复了断开的链接。不确定在这种情况下用永久链接指的是什么?
麦基·梅瑟

永久链接是网站永远不会更改的链接。因此,举例来说,大多数网站会代替第二个“永久链接”,例如“ www.SomeSite.com/events/today/item1.htm”(可能会每天更改),而不是每天都会更改。 12345',它永远不会改变,因此在链接中使用是安全的。并非每个人都这样做,但是大型网站的大多数页面都会提供一个页面。
Mark A. Donohoe

8

您可以为Dictionary类型编写自定义初始化程序,例如从元组中:

extension Dictionary {
    public init(keyValuePairs: [(Key, Value)]) {
        self.init()
        for pair in keyValuePairs {
            self[pair.0] = pair.1
        }
    }
}

然后map用于您的数组Person

var myDictionary = Dictionary(keyValuePairs: myArray.map{($0.position, $0.name)})

5

基于KeyPath的解决方案如何?

extension Array {

    func dictionary<Key, Value>(withKey key: KeyPath<Element, Key>, value: KeyPath<Element, Value>) -> [Key: Value] {
        return reduce(into: [:]) { dictionary, element in
            let key = element[keyPath: key]
            let value = element[keyPath: value]
            dictionary[key] = value
        }
    }
}

这是您将如何使用它:

struct HTTPHeader {

    let field: String, value: String
}

let headers = [
    HTTPHeader(field: "Accept", value: "application/json"),
    HTTPHeader(field: "User-Agent", value: "Safari"),
]

let allHTTPHeaderFields = headers.dictionary(withKey: \.field, value: \.value)

// allHTTPHeaderFields == ["Accept": "application/json", "User-Agent": "Safari"]

2

您可以使用reduce函数。首先,我为Person类创建了一个指定的初始化器

class Person {
  var name:String
  var position:Int

  init(_ n: String,_ p: Int) {
    name = n
    position = p
  }
}

后来,我初始化了一个值数组

let myArray = [Person("Bill",1), 
               Person("Steve", 2), 
               Person("Woz", 3)]

最后,字典变量具有以下结果:

let dictionary = myArray.reduce([Int: Person]()){
  (total, person) in
  var totalMutable = total
  totalMutable.updateValue(person, forKey: total.count)
  return totalMutable
}

2

这就是我一直在使用的

struct Person {
    let name:String
    let position:Int
}
let persons = [Person(name: "Franz", position: 1),
               Person(name: "Heinz", position: 2),
               Person(name: "Hans", position: 3)]

var peopleByPosition = [Int: Person]()
persons.forEach{peopleByPosition[$0.position] = $0}

如果有一种方法可以合并最后两行,那peopleByPosition将是一个不错的选择let

我们可以对Array做一个扩展!

extension Array {
    func mapToDict<T>(by block: (Element) -> T ) -> [T: Element] where T: Hashable {
        var map = [T: Element]()
        self.forEach{ map[block($0)] = $0 }
        return map
    }
}

那我们就可以做

let peopleByPosition = persons.mapToDict(by: {$0.position})

1
extension Array {
    func mapToDict<T>(by block: (Element) -> T ) -> [T: Element] where T: Hashable {
        var map = [T: Element]()
        self.forEach{ map[block($0)] = $0 }
        return map
    }
}

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.