Asignar una matriz de objetos al diccionario en Swift

94

Tengo una matriz de Personobjetos de:

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

y la matriz es:

let myArray = [p1,p1,p3]

Quiero mapear myArraypara ser un Diccionario de [position:name]la solución clásica es:

var myDictionary =  [Int:String]()

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

¿Hay alguna forma elegante de Swift de hacer eso con el enfoque funcional map, flatMap... u otro estilo Swift moderno?

iOSGeek
fuente
Un poco tarde para el juego, pero ¿quieres un diccionario de [position:name]o [position:[name]]? Si tiene dos personas en la misma posición, su diccionario solo mantendrá la última encontrada en su bucle ... Tengo una pregunta similar para la que estoy tratando de encontrar una solución, pero quiero que el resultado sea como[1: [p1, p3], 2: [p2]]
Zonker.in.Geneva
1
Su es una función incorporada. Vea aquí . Es básicamenteDictionary(uniqueKeysWithValues: array.map{ ($0.key, $0) })
Cariño

Respuestas:

128

Bueno, mapno es un buen ejemplo de esto, porque es lo mismo que hacer un bucle, puede usar reduceen su lugar, tomó cada uno de sus objetos para combinar y convertir en un valor único:

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

En Swift 4 o superior, utilice la siguiente respuesta para obtener una sintaxis más clara.

Tj3n
fuente
8
Me gusta este enfoque, pero ¿es más eficiente que simplemente crear un bucle para cargar un diccionario? Me preocupa el rendimiento porque parece que para cada elemento tiene que crear una nueva copia mutable de un diccionario cada vez más grande ...
Kendall Helmstetter Gelner
en realidad es un bucle, de esta manera es una forma simplificada de escribirlo, el rendimiento es el mismo o de alguna manera mejor que el bucle normal
Tj3n
2
Kendall, vea mi respuesta a continuación, puede hacerlo más rápido que el bucle cuando se usa Swift 4. Aunque no lo he evaluado para confirmarlo.
possen el
2
¡¡¡No uses esto !!! Como señaló @KendallHelmstetterGelner, el problema con este enfoque es cada iteración, está copiando todo el diccionario en su estado actual, por lo que en el primer ciclo, está copiando un diccionario de un elemento. La segunda iteración está copiando un diccionario de dos elementos. ¡Eso es una pérdida de rendimiento ENORME! La mejor manera de hacerlo sería escribir un inicio de conveniencia en el diccionario que tome su matriz y agregue todos los elementos en ella. De esa manera, sigue siendo una frase única para el consumidor, pero elimina todo el lío de asignación que tiene. Además, puede preasignar en función de la matriz.
Mark A. Donohoe
1
No creo que el rendimiento sea un problema. El compilador de Swift reconocerá que ninguna de las copias antiguas del diccionario se usa y probablemente ni siquiera las creará. Recuerde la primera regla para optimizar su propio código: "No lo haga". Otra máxima de la informática es que los algoritmos eficientes solo son útiles cuando N es grande y N casi nunca es grande.
bugloaf
136

Dado Swift 4que puede hacer el enfoque de @ Tj3n de manera más limpia y eficiente utilizando la intoversión de reduce, se elimina el diccionario temporal y el valor de retorno para que sea más rápido y fácil de leer.

Configuración de código de muestra:

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 el parámetro se pasa diccionario vacío del tipo de resultado:

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

Devuelve directamente un diccionario del tipo pasado into:

print(myDict) // [2: "c", 0: "h", 4: "b"]
possen
fuente
Estoy un poco confundido acerca de por qué solo parece funcionar con los argumentos posicionales ($ 0, $ 1) y no cuando los nombre. Editar: no importa, el compilador tal vez se tropezó porque todavía intenté devolver el resultado.
Departamento B
Lo que no está claro en esta respuesta es lo que $0representa: es el inoutdiccionario lo que estamos "devolviendo", aunque en realidad no regresamos, ya que modificarlo es equivalente a regresar debido a su carácter inout.
Future-Adam
94

Desde Swift 4 puedes hacer esto muy fácilmente. Hay dos nuevos inicializadores que crean un diccionario a partir de una secuencia de tuplas (pares de clave y valor). Si se garantiza que las claves son únicas, puede hacer lo siguiente:

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

Esto fallará con un error de tiempo de ejecución si se duplica alguna clave. En ese caso, puede utilizar esta versión:

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

Esto se comporta como su bucle for. Si no desea "sobrescribir" los valores y ceñirse al primer mapeo, puede usar esto:

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

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

Swift 4.2 agrega un tercer inicializador que agrupa elementos de secuencia en un diccionario. Las claves del diccionario se derivan de un cierre. Los elementos con la misma clave se colocan en una matriz en el mismo orden que en la secuencia. Esto le permite lograr resultados similares a los anteriores. Por ejemplo:

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
fuente
3
De hecho, de acuerdo con el inicializador de 'agrupación' de docs, crea un diccionario con una matriz como valor (eso significa 'agrupación', ¿verdad?), Y en el caso anterior será más parecido [1: [Person(name: "Franz", position: 1)], 2: [Person(name: "Heinz", position: 2)], 3: [Person(name: "Hans", position: 3)]]. No es el resultado esperado, pero puede mapearse más a un diccionario plano si lo desea.
Varrry
¡Eureka! ¡Este es el droide que estaba buscando! Esto es perfecto y simplifica enormemente las cosas en mi código.
Zonker.in.Geneva
Vínculos muertos a los inicializadores. ¿Puedes actualizar para usar el enlace permanente?
Mark A. Donohoe
@MarqueIV Arreglé los enlaces rotos. ¿No estás seguro de a qué te refieres con el enlace permanente en este contexto?
Mackie Messer
Los enlaces permanentes son enlaces que utilizan los sitios web y que nunca cambiarán. Entonces, por ejemplo, en lugar de algo como 'www.SomeSite.com/events/today/item1.htm' que puede cambiar día a día, la mayoría de los sitios establecerán un segundo 'enlace permanente' como 'www.SomeSite.com?eventid= 12345 'que nunca cambiará y, por lo tanto, es seguro de usar en enlaces. No todos lo hacen, pero la mayoría de las páginas de los grandes sitios ofrecerán uno.
Mark A. Donohoe
8

Puede escribir un inicializador personalizado para el Dictionarytipo, por ejemplo, de tuplas:

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

y luego úselo mappara su matriz de Person:

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

¿Qué tal una solución basada en 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
        }
    }
}

Así es como lo usará:

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
fuente
2

Puede utilizar una función de reducción. Primero he creado un inicializador designado para la clase Person

class Person {
  var name:String
  var position:Int

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

Más tarde, inicialicé una matriz de valores

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

Y finalmente, la variable de diccionario tiene el resultado:

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

Esto es lo que he estado usando

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}

Sería bueno si hubiera una manera de combinar las últimas 2 líneas para que peopleByPositionpudiera ser un let.

¡Podríamos hacer una extensión de Array que haga eso!

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

Entonces podemos hacer

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

¿Quizás algo como esto?

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