Есть ли декларативный способ преобразования массива в словарь?

Я хочу получить из этого массива строк

let entries = ["x=5", "y=7", "z=10"]

к этому

let keyValuePairs = ["x" : "5", "y" : "7", "z" : "10"]

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

Любые идеи?


person Earl Grey    schedule 21.02.2016    source источник
comment
Если строки в массиве всегда имеют один и тот же формат, вы можете проанализировать значение ключа, = и значение и назначить указанные данные словарю.   -  person Arc676    schedule 21.02.2016
comment
@ Arc676 Arc676, это императивный подход, конечно, он у меня уже есть .... но я ищу декларативное решение.   -  person Earl Grey    schedule 21.02.2016


Ответы (8)


Свифт 4

Как указано в fl034, это может быть упрощено с помощью Swift 4, где версия с проверкой ошибок выглядит так:

let foo = entries
    .map { $0.components(separatedBy: "=") }
    .reduce(into: [String:Int64]()) { dict, pair in
        if pair.count == 2, let value = Int64(pair[1]) {
            dict[pair[0]] = value
        }
    }

Еще проще, если вы не хотите, чтобы значения были целыми:

let foo = entries
    .map { $0.components(separatedBy: "=") }
    .reduce(into: [String:String]()) { dict, pair in
        if pair.count == 2 {
            dict[pair[0]] = pair[1]
        }
    }

Старый TL;DR

Минус проверка ошибок, это выглядит примерно так:

let foo = entries.map({ $0.componentsSeparatedByString("=") })
    .reduce([String:Int]()) { acc, comps in
        var ret = acc
        ret[comps[0]] = Int(comps[1])
        return ret
    }

Используйте карту, чтобы превратить [String] в разделенный [[String]], а затем создайте словарь [String:Int] из этого, используя сокращение.

Или, добавив расширение к Dictionary:

extension Dictionary {
    init(elements:[(Key, Value)]) {
        self.init()
        for (key, value) in elements {
            updateValue(value, forKey: key)
        }
    }
}

(Кстати, довольно полезное расширение, вы можете использовать его для множества операций с картами/фильтрами в словарях, очень жаль, что оно не существует по умолчанию)

Становится еще проще:

let dict = Dictionary(elements: entries
    .map({ $0.componentsSeparatedByString("=") })
    .map({ ($0[0], Int($0[1])!)})
)

Конечно, вы также можете комбинировать два вызова карты, но я предпочитаю разбивать отдельные преобразования.

Если вы хотите добавить проверку ошибок, можно использовать flatMap вместо map:

let dict2 = [String:Int](elements: entries
    .map({ $0.componentsSeparatedByString("=") })
    .flatMap({
        if $0.count == 2, let value = Int($0[1]) {
            return ($0[0], value)
        } else {
            return nil
        }})
)

Опять же, если вы хотите, вы, очевидно, можете объединить map в flatMap или разделить их для простоты.

let dict2 = [String:Int](elements: entries.flatMap {
    let parts = $0.componentsSeparatedByString("=")
    if parts.count == 2, let value = Int(parts[1]) {
        return (parts[0], value)
    } else {
        return nil
    }}
)
person David Berry    schedule 22.02.2016
comment
Из любопытства, для чего нужны сокращения acc, comps и ret? - person Slipp D. Thompson; 02.02.2017
comment
аккумулятор, комплектующие и обратка - person David Berry; 02.02.2017
comment
Посмотрите на мой ответ, так как Swift 4 делает все это устаревшим. - person heyfrank; 29.11.2017

Один из способов сделать это в два этапа с map и reduce с кортежем в качестве промежуточного значения, например:

let entries = ["x=5", "y=7", "z=10"]

let dict = entries.map { (str) -> (String, String) in
    let elements = str.characters.split("=").map(String.init)
    return (elements[0], elements[1])
    }.reduce([String:String]()) { (var dict, kvpair) in
        dict[kvpair.0] = kvpair.1
        return dict
}

for key in dict.keys {
    print("Value for key '\(key)' is \(dict[key]).")
}

выходы:

Value for key 'y' is Optional("7").
Value for key 'x' is Optional("5").
Value for key 'z' is Optional("10").

или с одним reduce с тем же выходом:

let entries = ["x=5", "y=7", "z=10"]

let dict = entries.reduce([String:String]()) { (var dict, entry) in
    let elements = entry.characters.split("=").map(String.init)
    dict[elements[0]] = elements[1]
    return dict
}

for key in dict.keys {
    print("Value for key '\(key)' is \(dict[key]).")
}
person Crowman    schedule 21.02.2016
comment
Отлично. Обратите внимание, что str.characters.split{$0 == "="} также может быть записано как str.characters.split("="). - person Luca Angeletti; 21.02.2016
comment
Также при закрытии первой карты вам не нужно объявлять var параметр str. Это может быть просто entries.map { (str) -> (String, String) in - person Luca Angeletti; 21.02.2016

Используйте новый метод reduce(into: Result) в Swift 4:

let keyValuePairs = entries.reduce(into: [String:String]()) { (dict, entry) in
    let key = String(entry.first!)
    let value = String(entry.last!)
    dict[entry.first!] = entry.last!
}

Конечно, разделение вашей строки можно улучшить.

person heyfrank    schedule 29.11.2017

Уже хорошие ответы. Вот дополнительный способ с расширением типа коллекции. Вы можете преобразовать любой тип коллекции в словарь, массив или набор.

extension CollectionType {
    func map2Dict<K, V>(@noescape map: ((Self.Generator.Element) -> (K,    V)?))  -> [K: V] {
        var d = [K: V]()
        for e in self {
            if let kV = map(e) {
                d[kV.0] = kV.1
            }
        }

        return d
    }

    func map2Array<T>(@noescape map: ((Self.Generator.Element) -> (T)?))  -> [T] {
        var a = [T]()
        for e in self {
            if let o = map(e) {
                a.append(o)
            }
        }

        return a
    }

    func map2Set<T>(@noescape map: ((Self.Generator.Element) -> (T)?))  -> Set<T> {
        return Set(map2Array(map))
    }
 }

Вот пример использования.

let entries = ["x=5", "y=7", "z=10"]
let dict:[String: String]  = entries.map2Dict({
let components = $0.componentsSeparatedByString("=")
    return (components.first!, components.last!)
})

print("dict \(dict)")
person user3255356    schedule 06.09.2016

Мне нравится ответ @paulgriffiths, но если у вас крайнее отвращение к ошибкам времени выполнения, вы можете сделать еще один шаг, чтобы убедиться, что каждая строка в исходном массиве действительно имеет оба обязательных элемента...

Важным отличием этого кода от других является то, что я проверяю, действительно ли в строке с элементами с обеих сторон есть знак "=". flatMap эффективно отфильтровывает все неудачные.

extension Dictionary {
    func withUpdate(key: Key, value: Value) -> Dictionary<Key, Value> {
        var result = self
        result[key] = value
        return result
    }
}

let entries = ["x=5", "y=7", "z=10"]

let keyValues = entries.flatMap { str -> (String, String)? in
    let split = str.characters.split("=").map(String.init)
    return split.count > 1 ? (split[0], split[1]) : nil
}

let keyValuePairs = keyValues.reduce([String: String]()) { $0.withUpdate($1.0, value: $1.1) }
person Daniel T.    schedule 18.06.2016

И для всех остальных, кто любит перегрузку и остроты.

public func +<K, V>(lhs: [K:V], rhs: [K:V]) -> [K:V] {
    var lhs: [K:V] = lhs
    for (key, value) in rhs {
        lhs[key] = value
    }
    return lhs
}

let array = ["x=5", "y=7", "z=10"]
let dictionary = array.map({ $0.componentsSeparatedByString("=") }).reduce([:]) { $0 + [$1[0]: $1[1]] }

print(dictionary) // ["y": "7", "x": "5", "z": "10"]
person Ian Bytchek    schedule 31.07.2016

Если у вас есть два параллельных массива, вы можете использовать zip(), Array() и расширение словаря Дэвида Берри:

let numbers = [10, 9, 8]
let strings = ["one", "two", "three", "four"]

let dictionary = Dictionary(elements: Array(zip(numbers, strings)))
// == [10: "one", 9: "two", 8: "three"]

Не забудьте добавить расширение:

extension Dictionary {
    init(elements:[(Key, Value)]) {
        self.init()
        for (key, value) in elements {
            updateValue(value, forKey: key)
        }
    }
}
person Senseful    schedule 11.06.2017

Простой способ:

let array = ["key1", "key2", "key3"]
let dict =  array.flatMap({["\($0)" : "value"]})

Выход:

[(ключ: "ключ1", значение: "значение"), (ключ: "ключ2", ​​значение: "значение"), (ключ: "ключ3", значение: "значение")]


Немного более сложный способ:

let array = ["key1", "key2", "key3"]
let dict =  array.enumerated().flatMap({["\($0.element)" : "value\($0.offset)"]})

Выход:

[(ключ: "ключ1", значение: "значение0"), (ключ: "ключ2", ​​значение: "значение1"), (ключ: "ключ3", значение: "значение2")]

person Dmitrii Z    schedule 22.09.2017