Какой самый чистый способ применения map () к словарю в Swift?

154

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

В этом примере я пытаюсь увеличить каждое значение на 1. Однако это случайно для примера - основная цель - выяснить, как применить map () к словарю.

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

d.map() {
    $0.1 += 1
}
Мария Зверина
источник
1
Словарь не имеет функции карты, поэтому неясно, что вы пытаетесь сделать.
Дэвид Берри
7
Да, я знаю, что в словаре нет функции карты. Вопрос может быть перефразирован так: как это можно сделать с помощью замыканий, без необходимости перебирать весь словарь.
Мария Зверина
2
вам все еще нужно перебирать весь словарь, так как это то, что mapделает. то, что вы просите, - это способ скрыть итерацию за расширением / закрытием, чтобы вам не приходилось смотреть на нее каждый раз, когда вы ее используете.
Джозеф Марк
1
Для Swift 4 см. Мой ответ, который показывает до 5 различных способов решения вашей проблемы.
Imanou Petit

Ответы:

281

Свифт 4+

Хорошие новости! Swift 4 включает в себя mapValues(_:)метод, который создает копию словаря с одинаковыми ключами, но разными значениями. Он также включает в себя filter(_:)перегрузку, которая возвращает инициализаторы a Dictionary, and init(uniqueKeysWithValues:)и init(_:uniquingKeysWith:)для создания a 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инициализатора, добавленного в одном из предыдущих примеров. Убедитесь, что вы включили их обоих.
Брент Роял-Гордон
66

С 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]
Imanou Petit
источник
Пожалуйста, пример, который меняет ключи, а не значения. Спасибо, это полезно.
Йогеш Патель
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
Недостатком этого подхода является то, что новая копия словаря создается при каждом добавлении нового ключа, поэтому эта функция ужасно неэффективна (оптимизатор может вас спасти).
Скорость
Это хороший момент ... одна вещь, которую я до сих пор не очень хорошо понимаю, это внутреннее управление памятью Swift. Я ценю такие комментарии, которые заставляют меня задуматься об этом :)
Аарон Расмуссен
1
В Swift 2.0 теперь нет необходимости в temp var и Reduce: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, карта является функцией массивов. Не для словарей.

Но вы можете перебрать свой словарь, чтобы изменить ключи:

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)
})
Antoine
источник
3

Swift 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

Swift 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

Затем просто установите _ =перед переменной.

Может быть, это поможет вам Спасибо.

NøBi Mac
источник
1

Я предполагаю, что проблема заключается в том, чтобы сохранить ключи нашего исходного словаря и каким-то образом отобразить все его значения.

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

Итак, с чего мы хотели бы начать, это словарь и замыкание (функция) и в итоге новый словарь. Проблема, конечно, в том, что строгая типизация Swift мешает. Замыкание должно указывать, какой тип входит и какой тип получается, и поэтому мы не можем создать метод, который принимает общее замыкание - кроме как в контексте универсального. Таким образом, нам нужна общая функция.

Другие решения сконцентрировались на том, чтобы делать это в общем мире самой общей структуры словаря, но я считаю, что проще думать с точки зрения функции верхнего уровня. Например, мы могли бы написать это, где 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], преобразовав значения первого словаря через замыкание.

(РЕДАКТИРОВАТЬ: теперь я вижу, что это фактически то же самое, что решение AirspeedVelocity, за исключением того, что я не добавил дополнительную элегантность инициализатора словаря, который делает словарь из zip-последовательности.)

матовый
источник
1

Swift 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
    }
}
dimpiax
источник
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": ...
Jeehut
источник
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
источник
Это была скорее концепция, чем реальный код, но, тем не менее, я расширил его до запуска кода.
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)

ссылка

Мухаммед Заид Патан
источник