Mapuj tablicę obiektów do Dictionary w Swift

101

Mam tablicę Personobiektów:

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

a tablica to:

let myArray = [p1,p1,p3]

Chcę mapować myArrayjako słownik [position:name]klasycznego rozwiązania:

var myDictionary =  [Int:String]()

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

Czy jest jakiś elegancki sposób przez Swift to zrobić z podejściem funkcjonalnym map, flatMap... lub inny nowoczesny styl Swift

iOSGeek
źródło
Trochę za późno do gry, ale czy potrzebujesz słownika [position:name]czy [position:[name]]? Jeśli masz dwie osoby na tej samej pozycji, Twój słownik zachowa tylko ostatnią napotkaną w Twojej pętli ... Mam podobne pytanie, na które próbuję znaleźć rozwiązanie, ale chcę, aby wynik był podobny[1: [p1, p3], 2: [p2]]
Zonker.in.Geneva
1
Mają wbudowaną funkcję. Zobacz tutaj . To w zasadzieDictionary(uniqueKeysWithValues: array.map{ ($0.key, $0) })
kochanie

Odpowiedzi:

134

Okay mapnie jest tego dobrym przykładem, ponieważ jest to to samo, co zapętlanie, możesz reducezamiast tego użyć, wymagało to połączenia każdego obiektu i przekształcenia go w pojedynczą wartość:

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

W wersji Swift 4 lub nowszej użyj poniższej odpowiedzi, aby uzyskać jaśniejszą składnię.

Tj3n
źródło
8
Podoba mi się to podejście, ale czy jest to bardziej wydajne niż zwykłe tworzenie pętli do ładowania słownika? Martwię się o wydajność, ponieważ wydaje się, że każda pozycja musi tworzyć nową, modyfikowalną kopię coraz większego słownika ...
Kendall Helmstetter Gelner
to właściwie pętla, w ten sposób jest uproszczony sposób jej zapisania, wydajność jest taka sama lub jakoś lepsza niż normalna pętla
Tj3n
2
Kendall, zobacz moją odpowiedź poniżej, może to przyspieszyć działanie niż zapętlenie podczas korzystania z Swift 4. Chociaż nie przeprowadziłem testów porównawczych, aby to potwierdzić.
possen
3
Nie używaj tego !!! Jak zauważył @KendallHelmstetterGelner, problem z tym podejściem polega na tym, że każda iteracja polega na kopiowaniu całego słownika w jego bieżącym stanie, więc w pierwszej pętli kopiuje słownik z jednym elementem. Druga iteracja polega na kopiowaniu słownika składającego się z dwóch pozycji. To OGROMNY spadek wydajności !! Lepszym sposobem na zrobienie tego byłoby napisanie wygodnego init w słowniku, który pobierze twoją tablicę i doda wszystkie jej elementy. W ten sposób jest to nadal jedna linijka dla konsumenta, ale eliminuje cały bałagan związany z przydzielaniem. Dodatkowo możesz wstępnie przydzielić na podstawie tablicy.
Mark A. Donohoe
2
Nie sądzę, żeby wydajność była problemem. Kompilator Swift rozpozna, że ​​żadna ze starych kopii słownika nie jest używana i prawdopodobnie nawet ich nie utworzy. Zapamiętaj pierwszą zasadę optymalizacji własnego kodu: „Nie rób tego”. Inną maksymą informatyki jest to, że wydajne algorytmy są przydatne tylko wtedy, gdy N jest duże, a N prawie nigdy nie jest duże.
bugloaf
149

Ponieważ Swift 4możesz wykonać podejście @ Tj3n bardziej czysto i wydajniej, używając intowersji reduceIt, pozbywa się tymczasowego słownika i wartości zwracanej, dzięki czemu jest szybszy i łatwiejszy do odczytania.

Przykładowa konfiguracja kodu:

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 parametr jest przekazywany pusty słownik typu wynikowego:

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

Bezpośrednio zwraca słownik typu przekazanego w into:

print(myDict) // [2: "c", 0: "h", 4: "b"]
possen
źródło
Jestem trochę zdezorientowany, dlaczego wydaje się, że działa tylko z argumentami pozycyjnymi ($ 0, $ 1), a nie kiedy je nazywam. Edycja: nieważne, kompilator mógł się potknąć, ponieważ nadal próbowałem zwrócić wynik.
Departamento B
Rzecz, która jest niejasna w tej odpowiedzi, to to, co $0reprezentuje: jest to inoutsłownik, do którego „zwracamy” - chociaż tak naprawdę nie inoutpowracamy , ponieważ modyfikowanie go jest równoważne zwracaniu z powodu jego -ości.
adamjansch
96

Od wersji Swift 4 możesz to zrobić bardzo łatwo. Istnieją dwa nowe inicjatory, które tworzą słownik z sekwencji krotek (pary klucza i wartości). Jeśli gwarantujemy, że klucze są unikatowe, możesz wykonać następujące czynności:

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

Jeśli jakikolwiek klucz zostanie zduplikowany, spowoduje to błąd w czasie wykonywania. W takim przypadku możesz użyć tej wersji:

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

Zachowuje się jak pętla for. Jeśli nie chcesz „zastępować” wartości i trzymać się pierwszego mapowania, możesz użyć tego:

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

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

Swift 4.2 dodaje trzeci inicjator, który grupuje elementy sekwencji w słowniku. Klucze słownika są wyprowadzane przez zamknięcie. Elementy z tym samym kluczem są umieszczane w tablicy w tej samej kolejności, co w sekwencji. Pozwala to na osiągnięcie podobnych rezultatów jak powyżej. Na przykład:

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

Mackie Messer
źródło
3
W rzeczywistości zgodnie z dokumentami inicjalizator „grupowanie” tworzy słownik z tablicą jako wartością (to znaczy „grupowanie” oznacza, prawda?), Aw powyższym przypadku będzie bardziej jak [1: [Person(name: "Franz", position: 1)], 2: [Person(name: "Heinz", position: 2)], 3: [Person(name: "Hans", position: 3)]]. Nie jest to oczekiwany wynik, ale jeśli chcesz, można go dalej odwzorować na słownik płaski.
Varrry,
Eureka! To jest robot, którego szukałem! Jest to doskonałe i znacznie upraszcza rzeczy w moim kodzie.
Zonker.in.Geneva
Niedziałające linki do inicjatorów. Czy możesz zaktualizować, aby używać permalink?
Mark A. Donohoe
@MarqueIV Naprawiłem zepsute linki. Nie wiesz, do czego odnosisz się w tym kontekście przez permalink ?
Mackie Messer
Linki stałe to linki używane przez witryny internetowe, które nigdy się nie zmienią. Na przykład zamiast czegoś takiego jak „www.SomeSite.com/events/today/item1.htm”, które może zmieniać się z dnia na dzień, większość witryn utworzy drugi „permalink”, taki jak „www.SomeSite.com?eventid= 12345 ', który nigdy się nie zmieni, dlatego można go bezpiecznie używać w linkach. Nie każdy to robi, ale większość stron z dużych witryn oferuje taką.
Mark A. Donohoe,
9

Możesz napisać niestandardowy inicjator dla Dictionarytypu, na przykład z krotek:

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

a następnie użyj mapdla swojej tablicy Person:

var myDictionary = Dictionary(keyValuePairs: myArray.map{($0.position, $0.name)})
Cień
źródło
5

A co z rozwiązaniem opartym na 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
        }
    }
}

Tak to wykorzystasz:

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
źródło
2

Możesz użyć funkcji zmniejszania. Najpierw utworzyłem wyznaczony inicjalizator dla klasy Person

class Person {
  var name:String
  var position:Int

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

Później zainicjowałem tablicę wartości

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

I wreszcie zmienna słownikowa ma wynik:

let dictionary = myArray.reduce([Int: Person]()){
  (total, person) in
  var totalMutable = total
  totalMutable.updateValue(person, forKey: total.count)
  return totalMutable
}
David
źródło
2

To jest to, czego używałem

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}

Byłoby miło, gdyby istniał sposób na połączenie ostatnich 2 wierszy, tak aby peopleByPositionmógł to być plik let.

Moglibyśmy stworzyć rozszerzenie Array, które to robi!

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

Wtedy możemy po prostu zrobić

let peopleByPosition = persons.mapToDict(by: {$0.position})
datinc
źródło
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
    }
}
prashant
źródło
1

Może coś takiego?

myArray.forEach({ myDictionary[$0.position] = $0.name })
Varun Goyal
źródło