Ordnen Sie das Array von Objekten dem Wörterbuch in Swift zu


93

Ich habe eine Reihe von PersonObjekten:

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

und das Array ist:

let myArray = [p1,p1,p3]

Ich möchte myArrayein Wörterbuch [position:name]der klassischen Lösung zuordnen:

var myDictionary =  [Int:String]()

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

Gibt es eine elegante Art und Weise, mit dem funktionellen Ansatz von Swift zu tun map, flatMap... oder anderem modernem Stil Swift


Ein bisschen spät zum Spiel, aber willst du ein Wörterbuch von [position:name]oder [position:[name]]? Wenn Sie zwei Personen an derselben Position haben, behält Ihr Wörterbuch nur die letzte in Ihrer Schleife gefundene Person bei. Ich habe eine ähnliche Frage, für die ich nach einer Lösung suche, aber ich möchte, dass das Ergebnis so aussieht[1: [p1, p3], 2: [p2]]
Zonker.in.Geneva

1
Ihre Funktion ist eingebaut. Siehe hier . Es ist im GrundeDictionary(uniqueKeysWithValues: array.map{ ($0.key, $0) })
Honey

Antworten:


127

Okay, mapist kein gutes Beispiel dafür, da es genau wie eine Schleife ist, die Sie reducestattdessen verwenden können. Es hat jedes Ihrer Objekte benötigt, um es zu kombinieren und in einen einzelnen Wert umzuwandeln:

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

In Swift 4 oder höher verwenden Sie bitte die folgende Antwort für eine klarere Syntax.


7
Ich mag diesen Ansatz, aber ist dies effizienter als das Erstellen einer Schleife zum Laden eines Wörterbuchs? Ich mache mir Sorgen um die Leistung, weil es so aussieht, als
müsste

Es ist eigentlich eine Schleife, diese Art ist vereinfacht, die Leistung ist die gleiche oder irgendwie besser als die normale Schleife
Tj3n

2
Kendall, siehe meine Antwort unten, es kann schneller sein als eine Schleife, wenn Swift 4 verwendet wird. Obwohl ich es nicht bewertet habe, um dies zu bestätigen.
Possen

2
Benutze das nicht !!! Wie @KendallHelmstetterGelner hervorhob, besteht das Problem bei diesem Ansatz in jeder Iteration, bei der das gesamte Wörterbuch in seinem aktuellen Zustand kopiert wird. In der ersten Schleife wird also ein Wörterbuch mit einem Element kopiert. Bei der zweiten Iteration wird ein Wörterbuch mit zwei Elementen kopiert. Das ist ein RIESIGER Leistungsverlust !! Der bessere Weg, dies zu tun, wäre, eine praktische Initiale in das Wörterbuch zu schreiben, die Ihr Array übernimmt und alle darin enthaltenen Elemente hinzufügt. Auf diese Weise ist es immer noch ein Einzeiler für den Verbraucher, aber es beseitigt das gesamte Allokationsproblem, das dies hat. Außerdem können Sie basierend auf dem Array eine Vorbelegung vornehmen.
Mark A. Donohoe

1
Ich denke nicht, dass Leistung ein Problem sein wird. Der Swift-Compiler erkennt, dass keine der alten Kopien des Wörterbuchs verwendet wird, und erstellt sie wahrscheinlich nicht einmal. Denken Sie an die erste Regel zur Optimierung Ihres eigenen Codes: "Tun Sie es nicht." Eine weitere Maxime der Informatik ist, dass effiziente Algorithmen nur dann nützlich sind, wenn N groß ist und N fast nie groß ist.
Bugloaf

135

Da Swift 4Sie den Ansatz von @ Tj3n mit der intoVersion von sauberer und effizienter ausführen können, werden reducedas temporäre Wörterbuch und der Rückgabewert entfernt, sodass es schneller und einfacher zu lesen ist.

Beispielcode-Setup:

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 Parameter wird leeres Wörterbuch des Ergebnistyps übergeben:

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

Gibt direkt ein Wörterbuch des übergebenen Typs zurück into:

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

Ich bin etwas verwirrt darüber, warum es nur mit den Positionsargumenten ($ 0, $ 1) zu funktionieren scheint und nicht, wenn ich sie benenne. Edit: egal, der Compiler ist vielleicht gestolpert, weil ich immer noch versucht habe, das Ergebnis zurückzugeben.
Departamento B

Die Sache, die in dieser Antwort unklar ist, ist das, was $0darstellt: Es ist das inoutWörterbuch, das wir "zurückgeben" - obwohl es nicht wirklich zurückkehrt, da das Modifizieren es dem Zurückgeben aufgrund seiner inout-ness entspricht.
Future-Adam

94

Seit Swift 4 können Sie dies sehr einfach tun. Es gibt zwei neue Initialisierer, die ein Wörterbuch aus einer Folge von Tupeln (Schlüssel- und Wertepaare) erstellen. Wenn die Schlüssel garantiert eindeutig sind, können Sie Folgendes tun:

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

Dies schlägt mit einem Laufzeitfehler fehl, wenn ein Schlüssel dupliziert wird. In diesem Fall können Sie diese Version verwenden:

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

Dies verhält sich wie Ihre for-Schleife. Wenn Sie keine Werte "überschreiben" und sich an die erste Zuordnung halten möchten, können Sie Folgendes verwenden:

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

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

Swift 4.2 fügt einen dritten Initialisierer hinzu, der Sequenzelemente in einem Wörterbuch gruppiert. Wörterbuchschlüssel werden durch einen Abschluss abgeleitet. Elemente mit demselben Schlüssel werden in derselben Reihenfolge wie in der Sequenz in ein Array eingefügt. Auf diese Weise können Sie ähnliche Ergebnisse wie oben erzielen. Zum Beispiel:

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
In der Tat erstellt der Initialisierer laut docs 'grouping' ein Wörterbuch mit einem Array als Wert (das bedeutet 'grouping', oder?), Und im obigen Fall wird es eher so sein [1: [Person(name: "Franz", position: 1)], 2: [Person(name: "Heinz", position: 2)], 3: [Person(name: "Hans", position: 3)]]. Nicht das erwartete Ergebnis, aber es kann weiter dem flachen Wörterbuch zugeordnet werden, wenn Sie dies wünschen.
Varrry

Eureka! Dies ist der Droide, nach dem ich gesucht habe! Dies ist perfekt und vereinfacht die Dinge in meinem Code erheblich.
Zonker.in.Geneva

Tote Links zu den Initialisierern. Können Sie bitte aktualisieren, um Permalink zu verwenden?
Mark A. Donohoe

@MarqueIV Ich habe die defekten Links behoben. Sie sind sich nicht sicher, worauf Sie sich in diesem Zusammenhang mit Permalink beziehen ?
Mackie Messer

Permalinks sind Links, die Websites verwenden und die sich niemals ändern werden. Anstelle von "www.SomeSite.com/events/today/item1.htm", das sich von Tag zu Tag ändern kann, richten die meisten Websites beispielsweise einen zweiten "Permalink" wie "www.SomeSite.com?eventid=" ein 12345 ', die sich nie ändern wird, und daher ist die Verwendung in Links sicher. Nicht jeder tut es, aber die meisten Seiten der großen Websites bieten eine an.
Mark A. Donohoe

8

Sie können einen benutzerdefinierten Initialisierer für den DictionaryTyp schreiben , z. B. aus Tupeln:

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

und verwenden Sie dann mapfür Ihr Array von Person:

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

4

Wie wäre es mit einer KeyPath-basierten Lösung?

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

So werden Sie es verwenden:

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

Sie können eine Reduzierungsfunktion verwenden. Zuerst habe ich einen bestimmten Initialisierer für die Personenklasse erstellt

class Person {
  var name:String
  var position:Int

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

Später habe ich ein Array von Werten initialisiert

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

Und schließlich hat die Wörterbuchvariable das Ergebnis:

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

1

Das habe ich benutzt

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}

Wäre schön, wenn es eine Möglichkeit gäbe, die letzten 2 Zeilen so zu kombinieren, dass peopleByPositiones eine sein könnte let.

Wir könnten eine Erweiterung für Array machen, die das macht!

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

Dann können wir es einfach tun

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

1

Vielleicht so etwas?

myArray.forEach({ myDictionary[$0.position] = $0.name })
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.