Отображение массива объектов в словарь в Swift

94

У меня есть массив 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

iOSGeek
источник
Немного поздно в игру, но вы хотите словарь [position:name]или [position:[name]]? Если у вас есть два человека в одной позиции, ваш словарь сохранит только последний из встреченных в вашем цикле ... У меня есть аналогичный вопрос, для которого я пытаюсь найти решение, но я хочу, чтобы результат был похож на[1: [p1, p3], 2: [p2]]
Zonker.in.Geneva
1
Их встроенная функция. Смотрите здесь . Это в основномDictionary(uniqueKeysWithValues: array.map{ ($0.key, $0) })
Дорогая

Ответы:

128

Хорошо, 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 или выше используйте ответ ниже для более четкого синтаксиса.

Tj3n
источник
8
Мне нравится этот подход, но является ли он более эффективным, чем просто создание цикла для загрузки словаря? Я беспокоюсь о производительности, потому что кажется, что для каждого элемента он должен создавать новую изменяемую копию все большего словаря ...
Кендалл Хельмштеттер Гелнер
на самом деле это цикл, это упрощенный способ его написания, производительность такая же или лучше, чем у обычного цикла
Tj3n
2
Кендалл, см. Мой ответ ниже, это может сделать его быстрее, чем цикл при использовании Swift 4. Хотя я не тестировал его, чтобы подтвердить это.
possen
2
Не используйте это !!! Как отметил @KendallHelmstetterGelner, проблема с этим подходом заключается в каждой итерации, он копирует весь словарь в его текущем состоянии, поэтому в первом цикле он копирует словарь с одним элементом. Вторая итерация - это копирование двухэлементного словаря. Это ОГРОМНАЯ потеря производительности !! Лучший способ сделать это - написать удобный init в словаре, который берет ваш массив и добавляет в него все элементы. Таким образом, это по-прежнему однострочный для потребителя, но он устраняет весь беспорядок с распределением, который он имеет. Кроме того, вы можете предварительно выделить на основе массива.
Марк А. Донохо
1
Я не думаю, что производительность будет проблемой. Компилятор Swift распознает, что ни одна из старых копий словаря не используется, и, вероятно, даже не будет их создавать. Помните первое правило оптимизации вашего собственного кода: «Не делайте этого». Другой принцип информатики состоит в том, что эффективные алгоритмы полезны только тогда, когда N велико, а N почти никогда не бывает большим.
bugloaf
136

Поскольку Swift 4вы можете использовать подход @Tj3n более чисто и эффективно, используя intoверсию, reduceон избавляется от временного словаря и возвращаемого значения, поэтому его легче и быстрее читать.

Пример настройки кода:

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"]
Possen
источник
Я немного смущен тем, почему он работает только с позиционными ($ 0, $ 1) аргументами, а не когда я их называю. Изменить: неважно, возможно, компилятор отключился, потому что я все еще пытался вернуть результат.
Departamento B
В этом ответе неясно то, что $0представляет: это inoutсловарь, который мы «возвращаем», хотя на самом деле не возвращаемся, поскольку его изменение эквивалентно возврату из-за его inout-ности.
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)]]. Не ожидаемый результат, но при желании он может быть преобразован в плоский словарь.
Varrry
Эврика! Это тот дроид, которого я искал! Это идеально и значительно упрощает мой код.
Zonker.in.Geneva
Мертвые ссылки на инициализаторы. Не могли бы вы обновить, чтобы использовать постоянную ссылку?
Марк А. Донохо
@MarqueIV Я исправил битые ссылки. Не уверены, что вы имеете в виду под постоянной ссылкой в этом контексте?
Mackie Messer
Постоянные ссылки - это ссылки, используемые веб-сайтами, которые никогда не изменятся. Так, например, вместо чего-то вроде www.SomeSite.com/events/today/item1.htm, которое может меняться изо дня в день, большинство сайтов установят вторую постоянную ссылку, например www.SomeSite.com?eventid= 12345 ', который никогда не изменится, поэтому его можно безопасно использовать в ссылках. Не все это делают, но большинство страниц крупных сайтов предлагают такую ​​возможность.
Марк А. Донохо
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"]
Lucamegh
источник
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могло быть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})
datinc
источник
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
    }
}
прашант
источник
1

Может как то так?

myArray.forEach({ myDictionary[$0.position] = $0.name })
Варун Гоял
источник