¿Cuál es la forma más limpia de aplicar map () a un diccionario en Swift?


154

Me gustaría asignar una función en todas las teclas del diccionario. Esperaba que algo como lo siguiente funcionara, pero el filtro no se puede aplicar al diccionario directamente. ¿Cuál es la forma más limpia de lograr esto?

En este ejemplo, estoy tratando de incrementar cada valor en 1. Sin embargo, esto es incidental para el ejemplo: el objetivo principal es descubrir cómo aplicar map () a un diccionario.

var d = ["foo" : 1, "bar" : 2]

d.map() {
    $0.1 += 1
}

1
El diccionario no tiene una función de mapa, por lo que no está claro qué está tratando de hacer.
David Berry

77
Sí, sé que Dictionary no tiene función de mapa. La pregunta podría reformularse como cómo se puede lograr esto con cierres sin necesidad de repetir todo el diccionario.
Maria Zverina

2
todavía necesita iterar sobre todo el diccionario, ya que eso es lo que maphace. lo que está pidiendo es una forma de ocultar la iteración detrás de una extensión / cierre para que no tenga que mirarla cada vez que la use.
Joseph Mark

1
Para Swift 4, vea mi respuesta que muestra hasta 5 formas diferentes de resolver su problema.
Imanou Petit

Respuestas:


281

Swift 4+

¡Buenas noticias! Swift 4 incluye un mapValues(_:)método que construye una copia de un diccionario con las mismas claves, pero con valores diferentes. También incluye una filter(_:)sobrecarga que devuelve un Dictionary, init(uniqueKeysWithValues:)e init(_:uniquingKeysWith:)inicializadores para crear un a Dictionarypartir de una secuencia arbitraria de tuplas. Eso significa que, si desea cambiar las claves y los valores, puede decir algo como:

let newDict = Dictionary(uniqueKeysWithValues:
    oldDict.map { key, value in (key.uppercased(), value.lowercased()) })

También hay nuevas API para fusionar diccionarios, sustituir un valor predeterminado por elementos faltantes, agrupar valores (convertir una colección en un diccionario de matrices, clave por el resultado de asignar la colección sobre alguna función), y más.

Durante la discusión de la propuesta, SE-0165 , que introdujo estas características, mencioné varias veces esta respuesta de Desbordamiento de pila, y creo que la gran cantidad de votos positivos ayudó a demostrar la demanda. ¡Gracias por su ayuda para mejorar Swift!


66
Ciertamente es imperfecto, pero no debemos dejar que lo perfecto sea enemigo de lo bueno. Una mapque pueda producir claves en conflicto sigue siendo útil mapen muchas situaciones. (Por otro lado, podría argumentar que mapear solo los valores es una interpretación natural de los mapdiccionarios; tanto el ejemplo del póster original como el mío solo modifican los valores, y conceptualmente, mapen una matriz solo modifica los valores, no los índices).
Brent Royal-Gordon

2
parece que falta su último bloque de código try:return Dictionary<Key, OutValue>(try map { (k, v) in (k, try transform(v)) })
jpsim

9
Actualizado para Swift 2.1? GettingDictionaryExtension.swift:13:16: Argument labels '(_:)' do not match any available overloads
Per Eriksson

44
Con Swift 2.2 estoy obteniendo Argument labels '(_:)' do not match any available overloadsal implementar esa última extensión. No estoy seguro de cómo resolverlo: /
Erken

2
@Audioy Ese fragmento de código depende del Dictionaryinicializador agregado en uno de los ejemplos anteriores. Asegúrate de incluir ambos.
Brent Royal-Gordon

65

Con Swift 5, puede usar uno de los cinco fragmentos siguientes para resolver su problema.


# 1 Utilizando el Dictionary mapValues(_:)método

let dictionary = ["foo": 1, "bar": 2, "baz": 5]

let newDictionary = dictionary.mapValues { value in
    return value + 1
}
//let newDictionary = dictionary.mapValues { $0 + 1 } // also works

print(newDictionary) // prints: ["baz": 6, "foo": 2, "bar": 3]

# 2 Usando Dictionary mapmétodo e init(uniqueKeysWithValues:)inicializador

let dictionary = ["foo": 1, "bar": 2, "baz": 5]

let tupleArray = dictionary.map { (key: String, value: Int) in
    return (key, value + 1)
}
//let tupleArray = dictionary.map { ($0, $1 + 1) } // also works

let newDictionary = Dictionary(uniqueKeysWithValues: tupleArray)

print(newDictionary) // prints: ["baz": 6, "foo": 2, "bar": 3]

# 3 Usando Dictionary reduce(_:_:)método o reduce(into:_:)método

let dictionary = ["foo": 1, "bar": 2, "baz": 5]

let newDictionary = dictionary.reduce([:]) { (partialResult: [String: Int], tuple: (key: String, value: Int)) in
    var result = partialResult
    result[tuple.key] = tuple.value + 1
    return result
}

print(newDictionary) // prints: ["baz": 6, "foo": 2, "bar": 3]
let dictionary = ["foo": 1, "bar": 2, "baz": 5]

let newDictionary = dictionary.reduce(into: [:]) { (result: inout [String: Int], tuple: (key: String, value: Int)) in
    result[tuple.key] = tuple.value + 1
}

print(newDictionary) // prints: ["baz": 6, "foo": 2, "bar": 3]

# 4. Usando Dictionary subscript(_:default:)subíndice

let dictionary = ["foo": 1, "bar": 2, "baz": 5]

var newDictionary = [String: Int]()
for (key, value) in dictionary {
    newDictionary[key, default: value] += 1
}

print(newDictionary) // prints: ["baz": 6, "foo": 2, "bar": 3]

# 5. Usando Dictionary subscript(_:)subíndice

let dictionary = ["foo": 1, "bar": 2, "baz": 5]

var newDictionary = [String: Int]()
for (key, value) in dictionary {
    newDictionary[key] = value + 1
}

print(newDictionary) // prints: ["baz": 6, "foo": 2, "bar": 3]

Por ejemplo, que cambian las claves, no los valores. Gracias es útil.
Yogesh Patel

18

Si bien la mayoría de las respuestas aquí se centran en cómo mapear todo el diccionario (claves y valores), la pregunta realmente solo quería mapear los valores. Esta es una distinción importante ya que los valores de mapeo le permiten garantizar el mismo número de entradas, mientras que el mapeo de clave y valor puede resultar en claves duplicadas.

Aquí hay una extensión mapValuesque le permite mapear solo los valores. Tenga en cuenta que también extiende el diccionario con inituna secuencia de pares clave / valor, que es un poco más general que inicializarlo desde una matriz:

extension Dictionary {
    init<S: SequenceType where S.Generator.Element == Element>
      (_ seq: S) {
        self.init()
        for (k,v) in seq {
            self[k] = v
        }
    }

    func mapValues<T>(transform: Value->T) -> Dictionary<Key,T> {
        return Dictionary<Key,T>(zip(self.keys, self.values.map(transform)))
    }

}

..¿Cómo se usa? ¿Soy nuevo en Swift Mind poniendo un ejemplo aquí?
FlowUI. SimpleUITesting.com

cuando probé myDictionary.map, dentro del cierre, tuve que hacer otro mapa de la manera habitual. Esto funciona, pero ¿es así como se supone que debe usarse?
FlowUI. SimpleUITesting.com

¿Existe alguna garantía de que el orden de las claves y los valores cuando se llaman por separado están emparejados y, por lo tanto, son adecuados para zip?
Aaron Zinman

¿Por qué crear un nuevo init cuando podrías usarlo func mapValues<T>(_ transform: (_ value: Value) -> T) -> [Key: T] { var result = [Key: T]() for (key, val) in self { result[key] = transform(val) } return result }? Me parece que también se lee mejor. ¿Hay algún problema de rendimiento?
Marcel

12

La forma más limpia es simplemente agregar mapal Diccionario:

extension Dictionary {
    mutating func map(transform: (key:KeyType, value:ValueType) -> (newValue:ValueType)) {
        for key in self.keys {
            var newValue = transform(key: key, value: self[key]!)
            self.updateValue(newValue, forKey: key)
        }
    }
}

Comprobando que funciona:

var dic = ["a": 50, "b": 60, "c": 70]

dic.map { $0.1 + 1 }

println(dic)

dic.map { (key, value) in
    if key == "a" {
        return value
    } else {
        return value * 2
    }
}

println(dic)

Salida:

[c: 71, a: 51, b: 61]
[c: 142, a: 51, b: 122]

1
El problema que veo con este enfoque es que SequenceType.mapno es una función mutante. Su ejemplo podría reescribirse para seguir el contrato existente map, pero eso es lo mismo que la respuesta aceptada.
Christopher Pickslay

7

También puede usar en reducelugar de mapa. reduce¡es capaz de hacer cualquier cosa que mappueda hacer y más!

let oldDict = ["old1": 1, "old2":2]

let newDict = reduce(oldDict, [String:Int]()) { dict, pair in
    var d = dict
    d["new\(pair.1)"] = pair.1
    return d
}

println(newDict)   //  ["new1": 1, "new2": 2]

Sería bastante fácil incluir esto en una extensión, pero incluso sin la extensión te permite hacer lo que quieras con una llamada de función.


10
La desventaja de este enfoque es que se hace una nueva copia del diccionario cada vez que agrega una nueva clave, por lo que esta función es terriblemente ineficiente (el optimizador podría salvarlo).
Velocidad de velocidad del aire

Ese es un buen punto ... una cosa que aún no entiendo muy bien es lo interno de la administración de memoria de Swift. Agradezco comentarios como este que me hacen pensar en ello :)
Aaron Rasmussen

1
Sin necesidad de temp var y reduce es ahora un método en Swift 2.0:let newDict = oldDict.reduce([String:Int]()) { (var dict, pair) in dict["new\(pair.1)"] = pair.1; return dict }
Zmey

4

Resulta que puedes hacer esto. Lo que debe hacer es crear una matriz a MapCollectionView<Dictionary<KeyType, ValueType>, KeyType>partir del keysmétodo devuelto por los diccionarios . ( Información aquí ) Luego puede asignar esta matriz y pasar los valores actualizados al diccionario.

var dictionary = ["foo" : 1, "bar" : 2]

Array(dictionary.keys).map() {
    dictionary.updateValue(dictionary[$0]! + 1, forKey: $0)
}

dictionary

¿Puede agregar una explicación sobre cómo convertir ["foo": 1, "bar": 2] a [("foo", 1), ("bar", 2)]
Maria Zverina

@MariaZverina No estoy seguro de lo que quieres decir.
Mick MacCallum

Si puede realizar la conversión anterior, el filtro se puede aplicar directamente a la matriz resultante
Maria Zverina

@MariaZverina Gotcha. Sí, desafortunadamente no conozco ninguna forma de hacerlo.
Mick MacCallum

3

De acuerdo con la Referencia de la biblioteca estándar de Swift, el mapa es una función de las matrices. No es para diccionarios.

Pero podría iterar su diccionario para modificar las claves:

var d = ["foo" : 1, "bar" : 2]

for (name, key) in d {
    d[name] = d[name]! + 1
}

3

Estaba buscando una manera de mapear un diccionario directamente en una matriz escrita con objetos personalizados. Encontró la solución en esta extensión:

extension Dictionary {
    func mapKeys<U> (transform: Key -> U) -> Array<U> {
        var results: Array<U> = []
        for k in self.keys {
            results.append(transform(k))
        }
        return results
    }

    func mapValues<U> (transform: Value -> U) -> Array<U> {
        var results: Array<U> = []
        for v in self.values {
            results.append(transform(v))
        }
        return results
    }

    func map<U> (transform: Value -> U) -> Array<U> {
        return self.mapValues(transform)
    }

    func map<U> (transform: (Key, Value) -> U) -> Array<U> {
        var results: Array<U> = []
        for k in self.keys {
            results.append(transform(k as Key, self[ k ]! as Value))
        }
        return results
    }

    func map<K: Hashable, V> (transform: (Key, Value) -> (K, V)) -> Dictionary<K, V> {
        var results: Dictionary<K, V> = [:]
        for k in self.keys {
            if let value = self[ k ] {
                let (u, w) = transform(k, value)
                results.updateValue(w, forKey: u)
            }
        }
        return results
    }
}

Usándolo de la siguiente manera:

self.values = values.map({ (key:String, value:NSNumber) -> VDLFilterValue in
    return VDLFilterValue(name: key, amount: value)
})

3

Swift 3

Intento una manera fácil en Swift 3.

Quiero mapa [Cadena: Cadena] a [Cadena: Cadena] , yo uso forEach en lugar de mapa o un mapa plano.

    let oldDict = ["key0": "val0", "key1": nil, "key1": "val2","key2": nil]
    var newDict = [String: String]()
    oldDict.forEach { (source: (key: String, value: String?)) in
        if let value = source.value{
            newDict[source.key] = value
        }
    }

Indeseable, ya que crea una nueva matriz y se le agrega
Steve Kuo,

2

Swift 5

La función de mapa del diccionario viene con esta sintaxis.

dictData.map(transform: ((key: String, value: String)) throws -> T)

solo puede establecer valores de cierre para esto

var dictData: [String: String] = [
    "key_1": "test 1",
    "key_2": "test 2",
    "key_3": "test 3",
    "key_4": "test 4",
    "key_5": "test 5",
]

dictData.map { (key, value) in
    print("Key :: \(key), Value :: \(value)")
}

La salida será ::

Key :: key_5, Value :: test 5
Key :: key_1, Value :: test 1
Key :: key_3, Value :: test 3
Key :: key_2, Value :: test 2
Key :: key_4, Value :: test 4

Se puede dar esta advertencia Result of call to 'map' is unused

Luego, simplemente establezca _ =antes de la variable.

Puede ser que te ayude. Gracias.


1

Supongo que el problema es mantener las claves de nuestro diccionario original y asignar todos sus valores de alguna manera.

Permítanos generalizar y decir que podemos querer asignar todos los valores a otro tipo .

Entonces, lo que nos gustaría comenzar es un diccionario y un cierre (una función) y terminar con un nuevo diccionario. El problema es, por supuesto, que la mecanografía estricta de Swift se interpone en el camino. Un cierre necesita especificar qué tipo entra y qué tipo sale, y por lo tanto no podemos hacer un método que tome un cierre general , excepto en el contexto de un genérico. Por lo tanto, necesitamos una función genérica.

Otras soluciones se han concentrado en hacer esto dentro del mundo genérico de la estructura genérica del Diccionario en sí, pero me resulta más fácil pensar en términos de una función de nivel superior. Por ejemplo, podríamos escribir esto, donde K es el tipo de clave, V1 es el tipo de valor del diccionario inicial y V2 es el tipo de valor del diccionario final:

func mapValues<K,V1,V2>(d1:[K:V1], closure:(V1)->V2) -> [K:V2] {
    var d2 = [K:V2]()
    for (key,value) in zip(d1.keys, d1.values.map(closure)) {
        d2.updateValue(value, forKey: key)
    }
    return d2
}

Aquí hay un ejemplo simple de llamarlo, solo para demostrar que el genérico realmente se resuelve en tiempo de compilación:

let d : [String:Int] = ["one":1, "two":2]
let result = mapValues(d) { (i : Int) -> String in String(i) }

Comenzamos con un diccionario de [String:Int]y terminamos con un diccionario de [String:String]transformando los valores del primer diccionario a través de un cierre.

(EDITAR: ahora veo que esto es efectivamente lo mismo que la solución AirspeedVelocity, excepto que no agregué la elegancia adicional de un inicializador de Diccionario que hace que un Diccionario sea una secuencia zip).


1

Swift 3

Uso:

let bob = ["a": "AAA", "b": "BBB", "c": "CCC"]
bob.mapDictionary { ($1, $0) } // ["BBB": "b", "CCC": "c", "AAA": "a"]

Extensión:

extension Dictionary {
    func mapDictionary(transform: (Key, Value) -> (Key, Value)?) -> Dictionary<Key, Value> {
        var dict = [Key: Value]()
        for key in keys {
            guard let value = self[key], let keyValue = transform(key, value) else {
                continue
            }

            dict[keyValue.0] = keyValue.1
        }
        return dict
    }
}

1

Si solo está tratando de asignar los valores (potencialmente cambiando su tipo), incluya esta extensión:

extension Dictionary {
    func valuesMapped<T>(_ transform: (Value) -> T) -> [Key: T] {
        var newDict = [Key: T]()
        for (key, value) in self {
            newDict[key] = transform(value)
        }
        return newDict
    }
}

Dado que tienes este diccionario:

let intsDict = ["One": 1, "Two": 2, "Three": 3]

La transformación del valor de una sola línea se ve así:

let stringsDict = intsDict.valuesMapped { String($0 * 2) }
// => ["One": "2", "Three": "6", "Two": "4"]

La transformación del valor de varias líneas se ve así:

let complexStringsDict = intsDict.valuesMapped { (value: Int) -> String in
    let calculationResult = (value * 3 + 7) % 5
    return String("Complex number #\(calculationResult)")
}
// => ["One": "Complex number #0", "Three": ...

0

Otro enfoque es para mapun diccionario y reduce, donde funciones keyTransformy valueTransformson funciones.

let dictionary = ["a": 1, "b": 2, "c": 3]

func keyTransform(key: String) -> Int {
    return Int(key.unicodeScalars.first!.value)
}

func valueTransform(value: Int) -> String {
    return String(value)
}

dictionary.map { (key, value) in
     [keyTransform(key): valueTransform(value)]
}.reduce([Int:String]()) { memo, element in
    var m = memo
    for (k, v) in element {
        m.updateValue(v, forKey: k)
    }
    return m
}

Era más un concepto que un código real, pero, sin embargo, lo expandí a código en ejecución.
NebulaFox

0

Swift 3 usé esto,

func mapDict(dict:[String:Any])->[String:String]{
    var updatedDict:[String:String] = [:]
    for key in dict.keys{
        if let value = dict[key]{
            updatedDict[key] = String(describing: value)
        }
    }

   return updatedDict
}

Uso:

let dict:[String:Any] = ["MyKey":1]
let mappedDict:[String:String] = mapDict(dict: dict)

Árbitro

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.