Який найчистіший спосіб застосувати map () до словника в Swift?


154

Я хотів би відобразити функцію на всіх клавішах словника. Я сподівався, що щось подібне спрацює, але фільтр не можна застосувати безпосередньо до словника. Який найчистіший спосіб досягти цього?

У цьому прикладі я намагаюсь збільшити кожне значення на 1. Однак для прикладу це випадково - головна мета - з'ясувати, як застосувати map () до словника.

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

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

1
Словник не має функції карти, тому не зрозуміло, що ви намагаєтеся зробити.
Девід Беррі

7
Так - я знаю, що словник не має функції карти. Запитання може бути переосмислено, як це можна зробити із закриттям, не потребуючи повторень над усім словником.
Марія Зверина

2
вам все-таки потрібно повторити весь словник, оскільки це mapробиться. те, що ви просите, - це спосіб приховати ітерацію за розширенням / закриттям, так що вам не доведеться дивитися на це кожен раз, коли ви його використовуєте.
Джозеф Марк

1
Для Swift 4 дивіться мою відповідь, яка показує до 5 різних способів вирішення вашої проблеми.
Іману Петі

Відповіді:


281

Швидкий 4+

Гарні новини! Swift 4 включає mapValues(_:)метод, який будує копію словника з однаковими клавішами, але різними значеннями. Вона також включає filter(_:)перевантаження, яка повертає a Dictionary, init(uniqueKeysWithValues:)та init(_:uniquingKeysWith:)ініціалізатори для створення Dictionaryз довільної послідовності кортежів. Це означає, що якщо ви хочете змінити і ключі, і значення, ви можете сказати щось на кшталт:

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

Існують також нові API для об'єднання словників разом, заміщення значення за замовчуванням для відсутніх елементів, групування значень (перетворення колекції у словник масивів, введене в ключ за результатом відображення колекції над деякою функцією) та інше.

Під час обговорення пропозиції SE-0165 , яка впровадила ці особливості, я кілька разів підніс цю відповідь на переповнення стека, і, думаю, велика кількість відгуків допомогла продемонструвати попит. Тож дякую за вашу допомогу, що робить Swift кращим!


6
Це, безумовно, недосконало, але ми не повинні дозволяти ідеалу бути ворогом добра. Те, mapщо може створити конфліктуючі ключі, все ще є корисним mapу багатьох ситуаціях. (З іншого боку, ви можете стверджувати, що відображення лише значень є природною інтерпретацією mapсловників. І приклад оригінального плаката, і мій лише змінюють значення, а концептуально, mapна масиві змінюються лише значення, а не індекси.)
Брент Роял-Гордон

2
у вашому останньому блоці коду, здається, відсутнє try:return Dictionary<Key, OutValue>(try map { (k, v) in (k, try transform(v)) })
jpsim

9
Оновлено для Swift 2.1? Як дістатисяDictionaryExtension.swift:13:16: Argument labels '(_:)' do not match any available overloads
Пер Еріксон

4
З Swift 2.2 я отримую, Argument labels '(_:)' do not match any available overloadsколи реалізую останнє розширення. Не дуже впевнений, як це вирішити: /
Еркен

2
@Audioy Цей фрагмент коду залежить від Dictionaryініціалізатора, доданого в одному з попередніх прикладів. Переконайтеся, що ви включили їх обох.
Brent Royal-Gordon

65

За допомогою Swift 5 ви можете використовувати один з п'яти наступних фрагментів , щоб вирішити свою проблему.


№1. Використання Dictionary mapValues(_:)методу

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. Використання Dictionary mapметоду та init(uniqueKeysWithValues:)ініціалізатора

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. Використання Dictionary reduce(_:_:)методу чи reduce(into:_:)методу

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. Використання Dictionary subscript(_:default:)індекса

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. Використання Dictionary subscript(_:)індекса

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]

Прикладіть, наприклад, що зміни ключів не є значеннями. Спасибі це корисно.
Йогеш Патель

18

Хоча більшість відповідей тут зосереджені на тому, як зіставити весь словник (ключі та значення), питання справді хотіло лише зіставити значення. Це важлива відмінність, оскільки відображення значень дозволяє гарантувати однакову кількість записів, тоді як відображення і ключа, і значення може призвести до повторення ключів.

Ось розширення, mapValuesяке дозволяє відображати лише значення. Зауважте, він також розширює словник із initпослідовності пар ключів / значень, що є трохи більш загальним, ніж ініціалізація його з масиву:

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

}

.. як воно використовується? Я новий для швидкого розуму, подаючи приклад тут?
FlowUI. SimpleUITesting.com

коли я спробував myDictionary.map, всередині закриття я повинен був зробити іншу карту регулярним шляхом. Це працює, але чи так це слід використовувати?
FlowUI. SimpleUITesting.com

Чи є десь гарантія того, що порядок ключів та значень, коли викликаються окремо, спарюються і, таким чином, підходять для zip?
Аарон Зінман

Навіщо створювати новий init, коли можна було просто використовувати func mapValues<T>(_ transform: (_ value: Value) -> T) -> [Key: T] { var result = [Key: T]() for (key, val) in self { result[key] = transform(val) } return result }. Я вважаю, що він також читає краще. Чи є проблема з продуктивністю?
Марсель

12

Найчистіший спосіб - просто додати mapдо словника:

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

Перевірка, що вона працює:

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)

Вихід:

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

1
Проблема, яку я бачу при такому підході, полягає в тому, що SequenceType.mapце не мутує функція. Ваш приклад можна переписати, щоб дотримуватися існуючого договору map, але це те саме, що прийнято відповідь.
Крістофер Пікслі

7

Ви також можете використовувати reduceзамість карти. reduceздатний робити все, що mapможна зробити, і більше!

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]

Зробити це досить просто в розширення, але навіть без розширення це дозволяє вам робити те, що ви хочете, за допомогою одного виклику функції.


10
Мінусом такого підходу є те, що абсолютно нова копія словника робиться щоразу, коли ви додаєте новий ключ, тому ця функція жахливо неефективна (оптимізатор може врятувати вас).
Швидкість руху повітря швидкості

Це хороший момент ... одне, що я не дуже добре розумію, - це внутрішня система управління пам’яттю Свіфта. Я вдячний за такі коментарі, які змушують задуматися над цим :)
Аарон Расмуссен

1
Немає необхідності в temp var і redudes - тепер метод у Swift 2.0:let newDict = oldDict.reduce([String:Int]()) { (var dict, pair) in dict["new\(pair.1)"] = pair.1; return dict }
Zmey

4

Виявляється, ти можеш це зробити. Що вам потрібно зробити, це створити масив із MapCollectionView<Dictionary<KeyType, ValueType>, KeyType>повернутого зі словників keysметоду. ( Інформація тут ) Потім можна зіставити цей масив і повернути оновлені значення назад до словника.

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

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

dictionary

Ви можете додати пояснення, як перетворити ["foo": 1, "bar": 2] в [("foo", 1), ("bar", 2)]
Марія Зверина,

@MariaZverina Я не впевнений, що ти маєш на увазі.
Мік Маккаллум

Якщо ви зможете виконати перетворення вище, фільтр можна застосувати безпосередньо до отриманого масиву
Марія Зверина

@MariaZverina Gotcha. Так, на жаль, я не знаю жодного способу зробити це.
Мік Маккаллум

3

Відповідно до стандартної бібліотечної бібліотеки Swift, map - це функція масивів. Не для словників.

Але ви можете повторити свій словник для зміни клавіш:

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

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

3

Я шукав спосіб зіставити словник прямо в набраний масив зі спеціальними об'єктами. Знайшли рішення в цьому розширенні:

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

Використовуючи його так:

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

3

Швидкий 3

Я спробую простий шлях у Swift 3.

Я хочу зіставити [String: String?] На [String: String] , я використовую forEach замість карти або плоскої карти.

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

Небажано, оскільки він створює новий масив і додає до нього
Стів Куо,

2

Швидкий 5

Функція карти словника поставляється з цим синтаксисом.

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

ви можете просто встановити значення закриття для цього

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

Вихід буде:

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

Це може бути попередженням Result of call to 'map' is unused

Потім просто встановити _ =перед змінною.

Можливо, це допоможе тобі. Дякую.


1

Я припускаю, що проблема полягає в тому, щоб зберегти ключі нашого оригінального словника і якось відобразити всі його значення.

Давайте узагальнимо і скажемо, що ми можемо захотіти перенести всі значення до іншого типу .

Отже, з чого ми хотіли б почати - це словник і закриття (функція), а в кінцевому підсумку новий словник. Проблема полягає, звичайно, у тому, що Суіфт чітко вводить текст. При закритті потрібно вказати, який тип входить і який тип виходить, і, таким чином, ми не можемо зробити метод, який займає загальне закриття - за винятком загального контексту. Таким чином, нам потрібна родова функція.

Інші рішення зосереджуються на тому, щоб це зробити в загальному світі самої родової структури словника, але мені легше думати з точки зору функції вищого рівня. Наприклад, ми могли б написати це, де K - тип ключа, V1 - тип значення стартового словника, а V2 - тип значення кінця словника:

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
}

Ось простий приклад його виклику, лише щоб довести, що загальне дійсно вирішує себе під час компіляції:

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

Ми почали зі словника [String:Int]і закінчили словником [String:String], трансформуючи значення першого словника через закриття.

(EDIT: Тепер я бачу, що це фактично те саме, що рішення AirspeedVelocity, за винятком того, що я не додав додаткової елегантності ініціалізатора словника, який створює словник із zip-послідовності.)


1

Швидкий 3

Використання:

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

Розширення:

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

Я ви лише намагаєтеся зіставити значення (потенційно змінюючи їх тип), включіть це розширення:

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

Враховуючи, що у вас є цей словник:

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

Потім однолінійне перетворення значення виглядає приблизно так:

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

Потім багатолінійне перетворення значення виглядає приблизно так:

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

Інший підхід полягає mapв словнику і reduce, де функцій keyTransformі valueTransformє функціями.

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

0

Swift 3 Я використав це,

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
}

Використання:

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

Реф

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.