Mapeie a matriz de objetos para o Dicionário em Swift

101

Eu tenho uma série de Personobjetos de:

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

e a matriz é:

let myArray = [p1,p1,p3]

Quero mapear myArraypara ser um Dicionário da [position:name]solução clássica:

var myDictionary =  [Int:String]()

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

existe alguma maneira elegante do Swift de fazer isso com a abordagem funcional map, flatMap... ou outro estilo moderno do Swift

iOSGeek
fonte
Um pouco tarde para o jogo, mas você quer um dicionário de [position:name]ou [position:[name]]? Se você tiver duas pessoas na mesma posição, seu dicionário manterá apenas a última encontrada em seu loop ... Tenho uma pergunta semelhante para a qual estou tentando encontrar uma solução, mas quero que o resultado seja como[1: [p1, p3], 2: [p2]]
Zonker.in.Geneva
1
Sua função é embutida. Veja aqui . É basicamenteDictionary(uniqueKeysWithValues: array.map{ ($0.key, $0) })
Honey,

Respostas:

133

Ok mapnão é um bom exemplo disso, porque é o mesmo que loop, você pode usar em reducevez disso, levou cada um de seus objetos para combinar e transformar em um único valor:

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"]

No Swift 4 ou superior, use a resposta abaixo para uma sintaxe mais clara.

Tj3n
fonte
8
Eu gosto dessa abordagem, mas ela é mais eficiente do que simplesmente criar um loop para carregar um dicionário? Eu me preocupo com o desempenho porque parece que para cada item é necessário criar uma nova cópia mutável de um dicionário cada vez maior ...
Kendall Helmstetter Gelner
na verdade é um loop, desta forma é uma maneira simplificada de escrevê-lo, o desempenho é o mesmo ou de alguma forma melhor do que o loop normal
Tj3n
2
Kendall, veja minha resposta abaixo, pode torná-lo mais rápido do que o loop ao usar o Swift 4. Embora eu não tenha feito o benchmarking para confirmar isso.
possivelmente em
3
Não use isso !!! Como @KendallHelmstetterGelner apontou, o problema com essa abordagem é a cada iteração, ela copia o dicionário inteiro em seu estado atual, portanto, no primeiro loop, está copiando um dicionário de um item. A segunda iteração está copiando um dicionário de dois itens. Isso é um dreno de desempenho ENORME !! A melhor maneira de fazer isso seria escrever um init de conveniência no dicionário que pega seu array e adiciona todos os itens nele. Dessa forma, ainda é uma linha única para o consumidor, mas elimina toda a confusão de alocação que isso tem. Além disso, você pode pré-alocar com base na matriz.
Mark A. Donohoe
2
Não acho que o desempenho seja um problema. O compilador Swift reconhecerá que nenhuma das cópias antigas do dicionário é usada e provavelmente nem mesmo as criará. Lembre-se da primeira regra para otimizar seu próprio código: "Não faça isso." Outra máxima da ciência da computação é que algoritmos eficientes só são úteis quando N é grande e N quase nunca é grande.
bugloaf
149

Como Swift 4você pode fazer a abordagem de @ Tj3n de forma mais limpa e eficiente usando a intoversão de reduceIt, livra-se do dicionário temporário e do valor de retorno, tornando-a mais rápida e fácil de ler.

Exemplo de configuração de código:

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 o parâmetro é passado para um dicionário vazio do tipo de resultado:

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

Retorna diretamente um dicionário do tipo passado em into:

print(myDict) // [2: "c", 0: "h", 4: "b"]
possen
fonte
Estou um pouco confuso sobre por que ele parece funcionar apenas com os argumentos posicionais ($ 0, $ 1) e não quando os nomeio. Edit: não importa, o compilador talvez tenha tropeçado porque eu ainda tentei retornar o resultado.
Departamento B de
O que não está claro nesta resposta é o que $0representa: é o inoutdicionário que estamos "devolvendo" - embora não retornando realmente, pois modificá-lo equivale a retornar devido a sua inout-ness.
adamjansch
96

Desde o Swift 4, você pode fazer isso facilmente. Existem dois novos inicializadores que constroem um dicionário a partir de uma sequência de tuplas (pares de chave e valor). Se as chaves forem exclusivas, você pode fazer o seguinte:

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"]

Isso falhará com um erro de tempo de execução se alguma chave for duplicada. Nesse caso, você pode usar esta versão:

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"]

Isso se comporta como seu loop for. Se você não quiser "sobrescrever" valores e se ater ao primeiro mapeamento, pode usar isto:

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

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

O Swift 4.2 adiciona um terceiro inicializador que agrupa os elementos da sequência em um dicionário. As chaves do dicionário são derivadas de um encerramento. Elementos com a mesma chave são colocados em uma matriz na mesma ordem que na sequência. Isso permite que você obtenha resultados semelhantes aos acima. Por exemplo:

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)]

Mackie Messer
fonte
3
De fato, de acordo com a documentação, o inicializador de 'agrupamento' cria um dicionário com array como valor (isso significa 'agrupamento', certo?), E no caso acima será mais parecido [1: [Person(name: "Franz", position: 1)], 2: [Person(name: "Heinz", position: 2)], 3: [Person(name: "Hans", position: 3)]]. Não é o resultado esperado, mas pode ser mapeado para um dicionário simples, se desejar.
Varrry
Eureka! Este é o andróide que estou procurando! Isso é perfeito e simplifica muito as coisas no meu código.
Zonker.in.Geneva
Links mortos para os inicializadores. Você pode atualizar para usar o link permanente?
Mark A. Donohoe
@MarqueIV Consertei os links quebrados. Não tem certeza do que você está se referindo com permalink neste contexto?
Mackie Messer
Permalinks são links usados ​​por sites que nunca serão alterados. Então, por exemplo, em vez de algo como 'www.SomeSite.com/events/today/item1.htm', que pode mudar de um dia para o outro, a maioria dos sites irá configurar um segundo 'permalink' como 'www.SomeSite.com?eventid= 12345 'que nunca mudará e, portanto, é seguro para usar em links. Nem todo mundo faz isso, mas a maioria das páginas dos grandes sites oferece um.
Mark A. Donohoe
8

Você pode escrever um inicializador personalizado para o Dictionarytipo, por exemplo, a partir de tuplas:

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

e, em seguida, use mappara sua matriz de Person:

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

Que tal uma solução baseada em 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
        }
    }
}

É assim que você vai usá-lo:

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"]
lucamegh
fonte
2

Você pode usar uma função de redução. Primeiro criei um inicializador designado para a classe Person

class Person {
  var name:String
  var position:Int

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

Mais tarde, inicializei uma matriz de valores

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

E, finalmente, a variável de dicionário tem o resultado:

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

Isso é o que tenho usado

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}

Seria bom se houvesse uma maneira de combinar as 2 últimas linhas de forma que peopleByPositionpudesse ser a let.

Poderíamos fazer uma extensão para Array que faça isso!

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
    }
}

Então podemos apenas fazer

let peopleByPosition = persons.mapToDict(by: {$0.position})
Datinc
fonte
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
    }
}
prashant
fonte
1

Talvez algo assim?

myArray.forEach({ myDictionary[$0.position] = $0.name })
Varun Goyal
fonte