Отображение массива объектов в словарь в 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


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


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"]

Я немного смущен тем, почему он работает только с позиционными ($ 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"]

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})

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.