Qual é a maneira mais limpa de aplicar map () a um dicionário no Swift?

154

Eu gostaria de mapear uma função em todas as teclas do dicionário. Eu esperava que algo como o seguinte funcionasse, mas o filtro não pode ser aplicado diretamente ao dicionário. Qual é a maneira mais limpa de conseguir isso?

Neste exemplo, estou tentando incrementar cada valor em 1. No entanto, isso é incidental para o exemplo - o objetivo principal é descobrir como aplicar map () a um dicionário.

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

d.map() {
    $0.1 += 1
}
Maria Zverina
fonte
1
O dicionário não possui uma função de mapa, portanto, não está claro o que você está tentando fazer.
David Berry
7
Sim - eu sei que o Dictionary não tem função de mapa. A questão poderia ser reformulada, como isso pode ser feito com fechamentos sem a necessidade de iterar o dicionário inteiro.
Maria Zverina
2
você ainda precisa percorrer todo o dicionário, pois é isso que a mapfaz. o que você está pedindo é uma maneira de ocultar a iteração atrás de uma extensão / fechamento, para que você não precise examiná-la toda vez que a usar.
Joseph Mark
1
Para o Swift 4, veja minha resposta que mostra até 5 maneiras diferentes de resolver seu problema.
Imanou Petit

Respostas:

281

Swift 4+

Boas notícias! O Swift 4 inclui um mapValues(_:)método que constrói uma cópia de um dicionário com as mesmas chaves, mas com valores diferentes. Também inclui uma filter(_:)sobrecarga que retorna um Dictionary, e init(uniqueKeysWithValues:)e init(_:uniquingKeysWith:)inicializadores para criar uma Dictionarya partir de uma sequência arbitrária de tuplos. Isso significa que, se você deseja alterar as chaves e os valores, pode dizer algo como:

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

Também existem novas APIs para mesclar dicionários, substituindo um valor padrão por elementos ausentes, agrupando valores (convertendo uma coleção em um dicionário de matrizes, codificado pelo resultado do mapeamento da coleção sobre alguma função) e muito mais.

Durante a discussão da proposta, SE-0165 , que introduziu esses recursos, eu trouxe essa resposta do Stack Overflow várias vezes e acho que o grande número de upvotes ajudou a demonstrar a demanda. Então, obrigado pela sua ajuda para tornar o Swift melhor!

Brent Royal-Gordon
fonte
6
Certamente é imperfeito, mas não devemos deixar que o perfeito seja o inimigo do bem. Um mapque poderia produzir chaves conflitantes ainda é útil mapem muitas situações. (Por outro lado, você poderia argumentar que o mapeamento apenas dos valores é uma interpretação natural dos mapdicionários - o exemplo do pôster original e o meu apenas modificam os valores e, conceitualmente, mapem uma matriz modifica apenas os valores, não os índices.)
Brent Royal-Gordon
2
parece que seu último bloco de código está faltando try:return Dictionary<Key, OutValue>(try map { (k, v) in (k, try transform(v)) })
jpsim 04/11/2015
9
Atualizado para o Swift 2.1? ObtendoDictionaryExtension.swift:13:16: Argument labels '(_:)' do not match any available overloads
Por Eriksson
4
Com o Swift 2.2, estou conseguindo Argument labels '(_:)' do not match any available overloadsimplementar essa última extensão. Não tenho muita certeza de como resolvê-lo: /
Erken 20/05
2
@ Audio Esse trecho de código depende do Dictionaryinicializador adicionado em um dos exemplos anteriores. Certifique-se de incluir os dois.
Brent Royal-Gordon
65

Com o Swift 5, você pode usar um dos cinco trechos a seguir para resolver seu problema.


# 1 Usando o 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 ou 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:)subscrito

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(_:)subscrito

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]
Imanou Petit
fonte
Por favor, exemplo que altera chaves e não valores. Obrigado útil.
Yogesh Patel
18

Enquanto a maioria das respostas aqui se concentra em como mapear o dicionário inteiro (chaves e valores), a pergunta realmente queria apenas mapear os valores. Essa é uma distinção importante, pois os valores de mapeamento permitem garantir o mesmo número de entradas, enquanto o mapeamento de chave e valor pode resultar em chaves duplicadas.

Aqui está uma extensão, mapValuesque permite mapear apenas os valores. Observe que ele também estende o dicionário com uma initsequência de pares de chave / valor, o que é um pouco mais geral do que inicializá-lo a partir de uma 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)))
    }

}
Velocidade da velocidade do ar
fonte
..como isso é usado? Sou novo no Swift Mind, colocando um exemplo aqui?
FlowUI. SimpleUITesting.com
quando tentei myDictionary.map, dentro do fechamento, tive que fazer outro mapa da maneira regular. Isso funciona, mas é assim que deveria ser usado?
FlowUI. SimpleUITesting.com
Existe uma garantia em algum lugar de que a ordem das chaves e valores, quando chamados separadamente, são emparelhados e, portanto, adequados ao zip?
Aaron Zinman
Por que criar um novo init quando você pode apenas usar func mapValues<T>(_ transform: (_ value: Value) -> T) -> [Key: T] { var result = [Key: T]() for (key, val) in self { result[key] = transform(val) } return result }. Acho que lê melhor também. Existe um problema de desempenho?
Marcel
12

A maneira mais limpa é adicionar mapao Dicionário:

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

Verificando se 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)

Resultado:

[c: 71, a: 51, b: 61]
[c: 142, a: 51, b: 122]
Joseph Mark
fonte
1
O problema que vejo com essa abordagem é que SequenceType.mapnão é uma função mutante. Seu exemplo pode ser reescrito para seguir o contrato existente map, mas é o mesmo que a resposta aceita.
226156 Christopher Pickslay
7

Você também pode usar em reducevez do mapa. reduceé capaz de fazer qualquer coisa mappode fazer e muito mais!

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]

Seria bastante fácil agrupar isso em um ramal, mas mesmo sem o ramal, você poderá fazer o que quiser com uma chamada de função.

Aaron Rasmussen
fonte
10
A desvantagem dessa abordagem é que uma cópia totalmente nova do dicionário é feita toda vez que você adiciona uma nova chave; portanto, essa função é terrivelmente ineficiente (o otimizador pode salvá-lo).
Velocidade da velocidade
Esse é um bom ponto ... uma coisa que ainda não entendi muito bem é o interior do gerenciamento de memória do Swift. Eu aprecio comentários como este que me fazem pensar nisso :)
Aaron Rasmussen
1
Não há necessidade de temperatura var e reduzir agora é um método em Swift 2.0:let newDict = oldDict.reduce([String:Int]()) { (var dict, pair) in dict["new\(pair.1)"] = pair.1; return dict }
Zmey
4

Acontece que você pode fazer isso. O que você precisa fazer é criar uma matriz a MapCollectionView<Dictionary<KeyType, ValueType>, KeyType>partir do keysmétodo retornado pelos dicionários . ( Informações aqui ) Você pode mapear essa matriz e passar os valores atualizados de volta ao dicionário.

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

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

dictionary
Mick MacCallum
fonte
Você pode por favor adicionar explicação como converter [ "foo": 1, "bar": 2] a [( "foo", 1), ( "bar", 2)]
Maria Zverina
@MariaZverina Não sei bem o que você quer dizer.
Mick MacCallum
Se você pode realizar a conversão acima, o filtro pode ser aplicado diretamente sobre a matriz resultante
Maria Zverina
@MariaZverina Gotcha. Sim, infelizmente, não estou ciente de nenhuma maneira de fazer isso.
Mick MacCallum
3

De acordo com a Referência da biblioteca padrão da Swift, o mapa é uma função de matrizes. Não para dicionários.

Mas você pode iterar seu dicionário para modificar as chaves:

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

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

Eu estava procurando uma maneira de mapear um dicionário diretamente em uma matriz digitada com objetos personalizados. Encontrou a solução nesta extensão:

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

Utilizando-o da seguinte maneira:

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

Swift 3

Eu tento uma maneira fácil no Swift 3.

Quero mapear [String: String?] Para [String: String] , uso forEach em vez de map ou flat map.

    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
        }
    }
Companheiro
fonte
Indesejável, pois cria uma nova matriz e anexa a ela
Steve Kuo
2

Swift 5

A função map do dicionário vem com esta sintaxe.

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

você pode apenas definir valores de fechamento para isso

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

A saída 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

Pode ser esse aviso Result of call to 'map' is unused

Em seguida, basta definir _ =antes da variável.

Pode ser que ele irá ajudá-lo Obrigado.

NøBi Mac
fonte
1

Suponho que o problema é manter as chaves do nosso dicionário original e mapear todos os seus valores de alguma forma.

Vamos generalizar e dizer que podemos querer mapear todos os valores para outro tipo .

Então, o que gostaríamos de começar é um dicionário e um fechamento (uma função) e terminamos com um novo dicionário. O problema é, é claro, que a digitação estrita de Swift atrapalha. Um fechamento precisa especificar qual tipo entra e qual sai, e, portanto, não podemos criar um método que faça um fechamento geral - exceto no contexto de um genérico. Portanto, precisamos de uma função genérica.

Outras soluções se concentraram em fazer isso dentro do mundo genérico da estrutura genérica do Dicionário, mas acho mais fácil pensar em termos de uma função de nível superior. Por exemplo, podemos escrever isso, onde K é o tipo de chave, V1 é o tipo de valor do dicionário inicial e V2 é o tipo de valor do dicionário 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
}

Aqui está um exemplo simples de chamá-lo, apenas para provar que o genérico realmente se resolve em tempo de compilação:

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

Começamos com um dicionário de [String:Int]e terminamos com um dicionário de [String:String], transformando os valores do primeiro dicionário por meio de um fechamento.

(EDIT: Agora vejo que isso é efetivamente o mesmo que a solução da AirspeedVelocity, exceto que eu não adicionei a elegância extra de um inicializador de dicionário que transforma um dicionário em uma sequência zip.)

mate
fonte
1

Swift 3

Uso:

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

Extensão:

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
    }
}
dimpiax
fonte
1

Se você está apenas tentando mapear os valores (potencialmente alterando seu tipo), inclua esta extensão:

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

Como você tem este dicionário:

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

A transformação de valor de linha única fica assim:

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

A transformação de valor de várias linhas fica assim:

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

Outra abordagem é a mapde um dicionário e reduce, em que funções keyTransforme valueTransformsão funções.

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
}
NebulaFox
fonte
Era mais um conceito do que um código real, mas mesmo assim eu o expandi para o código em execução.
NebulaFox 01/12/2015
0

Swift 3 eu usei isso,

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)

Ref.

Mohammad Zaid Pathan
fonte