Зіставити масив об’єктів зі словником у Swift


95

У мене є масив Personоб’єктів:

class Person {
   let name:String
   let position:Int
}

а масив:

let myArray = [p1,p1,p3]

Я хочу зіставити myArrayсловник [position:name]класичним рішенням:

var myDictionary =  [Int:String]()

for person in myArray {
    myDictionary[person.position] = person.name
}

чи є елегантний спосіб Свіфт , щоб зробити це з функціональним підходом map, flatMap... або інший сучасний стиль Swift


Трохи пізно до гри, але вам потрібен словник [position:name]або [position:[name]]? Якщо у вас двоє людей в однаковій позиції, ваш словник буде зберігати лише останнього, що зустрічався у вашому циклі .... У мене є подібне запитання, для якого я намагаюся знайти рішення, але я хочу, щоб результат був таким[1: [p1, p3], 2: [p2]]
Zonker.in.Geneva

1
Їх вбудована функція. Дивіться тут . Це в основномуDictionary(uniqueKeysWithValues: array.map{ ($0.key, $0) })
Мед

Відповіді:


129

Добре, mapце не хороший приклад цього, тому що це так само, як цикл, ви можете використовувати reduceзамість нього, кожен ваш об'єкт знадобився для об'єднання та перетворення в єдине значення:

let myDictionary = myArray.reduce([Int: String]()) { (dict, person) -> [Int: String] in
    var dict = dict
    dict[person.position] = person.name
    return dict
}

//[2: "b", 3: "c", 1: "a"]

У версії Swift 4 або новішої, будь ласка, використовуйте відповідь нижче для чіткішого синтаксису.


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

це насправді цикл, це спрощений спосіб його написання, продуктивність однакова або якось краща за звичайний цикл
Tj3n

2
Кендалл, див. Мою відповідь нижче, це може зробити це швидше, ніж цикл при використанні Swift 4. Хоча, я не проводив порівняльний аналіз, щоб підтвердити це.
поссен

2
Не використовуйте це !!! Як зазначив @KendallHelmstetterGelner, проблемою цього підходу є кожна ітерація, це копіювання всього словника в його поточному стані, тому в першому циклі це копіювання словника з одним елементом. Друга ітерація - це копіювання словника з двох елементів. Це ВЕЛИЧЕЗНИЙ злив продуктивності !! Кращим способом це було б написати зручний init у словнику, який бере ваш масив і додає всі елементи до нього. Таким чином, він як і раніше залишається однокласним для споживача, але усуває всю нерозподіленість, яку він має. Крім того, ви можете попередньо розподілити на основі масиву.
Mark A. Donohoe

1
Я не думаю, що продуктивність буде проблемою. Компілятор Swift визнає, що жодна зі старих копій словника не звикає, і, ймовірно, навіть не створить їх. Запам’ятайте перше правило оптимізації власного коду: «Не роби цього». Ще одна максима інформатики полягає в тому, що ефективні алгоритми корисні лише тоді, коли N велике, а N майже ніколи велике.
bugloaf

138

Оскільки Swift 4ви можете зробити підхід @ Tj3n більш чисто і ефективно, використовуючи intoверсію reduceIt, позбавляється від тимчасового словника та поверненого значення, тому його швидше та легше читати.

Зразок налаштування коду:

struct Person { 
    let name: String
    let position: Int
}
let myArray = [Person(name:"h", position: 0), Person(name:"b", position:4), Person(name:"c", position:2)]

Into параметр передано порожній словник типу результату:

let myDict = myArray.reduce(into: [Int: String]()) {
    $0[$1.position] = $1.name
}

Безпосередньо повертає словник типу, переданого в into:

print(myDict) // [2: "c", 0: "h", 4: "b"]

Я трохи збентежений, чому це, здається, працює лише з позиційними ($ 0, $ 1) аргументами, а не тоді, коли я їх називаю. Edit: нічого, компілятор, можливо, зіткнувся, тому що я все-таки намагався повернути результат.
Departamento B

Незрозуміле в цій відповіді полягає в тому, що $0представляє: саме inoutсловник ми «повертаємося» - хоча насправді не повертаємось, оскільки його зміна еквівалентно поверненню через це inout-ness.
future-adam

94

З Swift 4 ви можете зробити це дуже легко. Є два нових ініціалізатори, які будують словник із послідовності кортежів (пар ключа і значення). Якщо ключі гарантовано унікальні, ви можете зробити наступне:

let persons = [Person(name: "Franz", position: 1),
               Person(name: "Heinz", position: 2),
               Person(name: "Hans", position: 3)]

Dictionary(uniqueKeysWithValues: persons.map { ($0.position, $0.name) })

=> [1: "Franz", 2: "Heinz", 3: "Hans"]

Це не вдасться під час виконання помилки, якщо будь-який ключ продубльовано. У цьому випадку ви можете використовувати цю версію:

let persons = [Person(name: "Franz", position: 1),
               Person(name: "Heinz", position: 2),
               Person(name: "Hans", position: 1)]

Dictionary(persons.map { ($0.position, $0.name) }) { _, last in last }

=> [1: "Hans", 2: "Heinz"]

Це поводиться як ваш цикл for. Якщо ви не хочете "перезаписувати" значення і дотримуватися першого відображення, ви можете використовувати це:

Dictionary(persons.map { ($0.position, $0.name) }) { first, _ in first }

=> [1: "Franz", 2: "Heinz"]

Swift 4.2 додає третій ініціалізатор, який групує елементи послідовності у словник. Ключі словника отримуються шляхом закриття. Елементи з однаковим ключем поміщаються в масив у тому самому порядку, що і в послідовності. Це дозволяє досягти подібних результатів, як зазначено вище. Наприклад:

Dictionary(grouping: persons, by: { $0.position }).mapValues { $0.last! }

=> [1: Person(name: "Hans", position: 1), 2: Person(name: "Heinz", position: 2)]

Dictionary(grouping: persons, by: { $0.position }).mapValues { $0.first! }

=> [1: Person(name: "Franz", position: 1), 2: Person(name: "Heinz", position: 2)]


3
Насправді згідно з документами ініціалізатор «групування» створює словник із масивом як значенням (це означає «групування», так?), І у наведеному вище випадку це буде більше схоже [1: [Person(name: "Franz", position: 1)], 2: [Person(name: "Heinz", position: 2)], 3: [Person(name: "Hans", position: 3)]]. Не очікуваний результат, але за бажанням його можна порівняти далі до плоского словника.
Варрі

Еврика! Це той дроїд, якого я шукав! Це ідеально і значно спрощує речі в моєму коді.
Zonker.in.Geneva

Мертві посилання на ініціалізатори. Чи можете ви оновити, щоб використовувати постійне посилання?
Mark A.

@MarqueIV Я виправив непрацюючі посилання. Не впевнені, на що ви посилаєтесь із постійним посиланням у цьому контексті?
Маккі Мессер

Постійні посилання - це посилання, які використовують веб-сайти, і які ніколи не зміняться. Так, наприклад, замість чогось на кшталт 'www.SomeSite.com/events/today/item1.htm', яке може змінюватися щодня, більшість сайтів встановлюють друге "постійне посилання", наприклад "www.SomeSite.com?eventid= 12345 ', який ніколи не зміниться, і, отже, його безпечно використовувати у посиланнях. Не кожен це робить, але більшість сторінок великих сайтів запропонують одну.
Mark A.

8

Ви можете написати власний ініціалізатор для Dictionaryтипу, наприклад, з кортежів:

extension Dictionary {
    public init(keyValuePairs: [(Key, Value)]) {
        self.init()
        for pair in keyValuePairs {
            self[pair.0] = pair.1
        }
    }
}

а потім використовувати mapдля вашого масиву Person:

var myDictionary = Dictionary(keyValuePairs: myArray.map{($0.position, $0.name)})

5

Як щодо рішення на основі KeyPath?

extension Array {

    func dictionary<Key, Value>(withKey key: KeyPath<Element, Key>, value: KeyPath<Element, Value>) -> [Key: Value] {
        return reduce(into: [:]) { dictionary, element in
            let key = element[keyPath: key]
            let value = element[keyPath: value]
            dictionary[key] = value
        }
    }
}

Ось як ви будете ним користуватися:

struct HTTPHeader {

    let field: String, value: String
}

let headers = [
    HTTPHeader(field: "Accept", value: "application/json"),
    HTTPHeader(field: "User-Agent", value: "Safari"),
]

let allHTTPHeaderFields = headers.dictionary(withKey: \.field, value: \.value)

// allHTTPHeaderFields == ["Accept": "application/json", "User-Agent": "Safari"]

2

Ви можете використовувати функцію зменшення. Спочатку я створив призначений ініціалізатор для класу Person

class Person {
  var name:String
  var position:Int

  init(_ n: String,_ p: Int) {
    name = n
    position = p
  }
}

Пізніше я ініціалізував масив значень

let myArray = [Person("Bill",1), 
               Person("Steve", 2), 
               Person("Woz", 3)]

І нарешті, змінна словника має результат:

let dictionary = myArray.reduce([Int: Person]()){
  (total, person) in
  var totalMutable = total
  totalMutable.updateValue(person, forKey: total.count)
  return totalMutable
}

1

Це те, що я використовував

struct Person {
    let name:String
    let position:Int
}
let persons = [Person(name: "Franz", position: 1),
               Person(name: "Heinz", position: 2),
               Person(name: "Hans", position: 3)]

var peopleByPosition = [Int: Person]()
persons.forEach{peopleByPosition[$0.position] = $0}

Було б непогано, якби був спосіб поєднати останні 2 рядки, щоб це peopleByPositionмогло бути a let.

Ми могли б зробити розширення для Array, яке це робить!

extension Array {
    func mapToDict<T>(by block: (Element) -> T ) -> [T: Element] where T: Hashable {
        var map = [T: Element]()
        self.forEach{ map[block($0)] = $0 }
        return map
    }
}

Тоді ми можемо просто зробити

let peopleByPosition = persons.mapToDict(by: {$0.position})

1
extension Array {
    func mapToDict<T>(by block: (Element) -> T ) -> [T: Element] where T: Hashable {
        var map = [T: Element]()
        self.forEach{ map[block($0)] = $0 }
        return map
    }
}

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