Зіставити масив об’єктів зі словником у 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.