Memetakan array objek ke Dictionary di Swift

94

Saya memiliki array Personobjek:

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

dan arraynya adalah:

let myArray = [p1,p1,p3]

Saya ingin memetakan myArraymenjadi kamus [position:name]solusi klasik adalah:

var myDictionary =  [Int:String]()

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

adakah cara elegan Swift untuk melakukannya dengan pendekatan fungsional map, flatMap... atau gaya Swift modern lainnya

iOSGeek
sumber
Sedikit terlambat ke permainan, tapi apakah Anda ingin kamus [position:name]atau [position:[name]]? Jika Anda memiliki dua orang di posisi yang sama, kamus Anda hanya akan menyimpan orang terakhir yang Anda temui dalam lingkaran Anda .... Saya memiliki pertanyaan serupa yang saya coba cari solusinya, tetapi saya ingin hasilnya seperti[1: [p1, p3], 2: [p2]]
Zonker.in.Geneva
1
Mereka adalah fungsi bawaan. Lihat disini . Pada dasarnyaDictionary(uniqueKeysWithValues: array.map{ ($0.key, $0) })
Madu

Jawaban:

129

Oke mapbukan contoh yang baik untuk ini, karena ini sama dengan perulangan, Anda dapat menggunakan reducesebagai gantinya, itu mengambil setiap objek Anda untuk digabungkan dan berubah menjadi nilai tunggal:

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

Di Swift 4 atau lebih tinggi, gunakan jawaban di bawah ini untuk sintaks yang lebih jelas.

Tj3n
sumber
8
Saya suka pendekatan ini, tetapi apakah ini lebih efisien daripada sekadar membuat loop untuk memuat kamus? Saya khawatir tentang kinerja karena tampaknya untuk setiap item itu harus membuat salinan baru yang bisa berubah dari kamus yang lebih besar ...
Kendall Helmstetter Gelner
ini sebenarnya sebuah loop, cara ini adalah cara yang disederhanakan untuk menulisnya, kinerjanya sama atau entah bagaimana lebih baik dari loop normal
Tj3n
2
Kendall, lihat jawaban saya di bawah ini, mungkin membuatnya lebih cepat daripada looping saat menggunakan Swift 4. Meskipun, saya belum melakukan benchmark untuk memastikannya.
possen
2
Jangan gunakan ini !!! Seperti yang ditunjukkan oleh @KendallHelmstetterGelner, masalah dengan pendekatan ini adalah setiap iterasi, itu menyalin seluruh kamus dalam keadaannya saat ini, jadi di loop pertama, itu menyalin kamus satu item. Iterasi kedua adalah menyalin kamus dua item. Itu menguras kinerja BESAR !! Cara yang lebih baik untuk melakukannya adalah dengan menulis init yang praktis di kamus yang mengambil larik Anda dan menambahkan semua item di dalamnya. Dengan begitu, ini masih satu baris untuk konsumen, tetapi menghilangkan seluruh kekacauan alokasi yang dimilikinya. Selain itu, Anda dapat mengalokasikan sebelumnya berdasarkan larik.
Mark A. Donohoe
1
Saya tidak berpikir kinerja akan menjadi masalah. Kompilator Swift akan mengenali bahwa tidak ada salinan lama kamus yang digunakan dan bahkan mungkin tidak akan membuatnya. Ingat aturan pertama dalam mengoptimalkan kode Anda sendiri: "Jangan lakukan itu". Pepatah lain dari ilmu komputer adalah bahwa algoritma yang efisien hanya berguna jika N besar, dan N hampir tidak pernah besar.
bugloaf
137

Karena Swift 4Anda dapat melakukan pendekatan @ Tj3n dengan lebih bersih dan efisien menggunakan intoversi reduceIni menyingkirkan kamus sementara dan nilai kembalian sehingga lebih cepat dan lebih mudah untuk dibaca.

Contoh kode 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 dilewatkan kamus kosong dari jenis hasil:

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

Mengembalikan secara langsung kamus dari tipe yang diteruskan into:

print(myDict) // [2: "c", 0: "h", 4: "b"]
possen
sumber
Saya agak bingung tentang mengapa tampaknya hanya berfungsi dengan argumen posisi ($ 0, $ 1) dan bukan ketika saya menamainya. Sunting: lupakan, kompilator mungkin tersandung karena saya masih mencoba mengembalikan hasilnya.
Departamento B
Hal yang tidak jelas dalam jawaban ini adalah apa yang $0merepresentasikan: inoutkamus itulah yang kita "kembalikan" - meskipun tidak benar-benar kembali, karena memodifikasinya sama dengan mengembalikan karena itu inout-ness.
masa depan adam
94

Sejak Swift 4 Anda dapat melakukan ini dengan sangat mudah. Ada dua penginisialisasi baru yang membangun kamus dari urutan tupel (pasangan kunci dan nilai). Jika kunci dijamin unik, Anda dapat melakukan hal berikut:

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

Ini akan gagal dengan error runtime jika ada kunci yang diduplikasi. Dalam hal ini Anda dapat menggunakan versi ini:

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

Ini berperilaku sebagai loop for Anda. Jika Anda tidak ingin "menimpa" nilai dan tetap menggunakan pemetaan pertama, Anda dapat menggunakan ini:

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

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

Swift 4.2 menambahkan penginisialisasi ketiga yang mengelompokkan elemen urutan ke dalam kamus. Kunci kamus diturunkan dengan penutupan. Elemen dengan kunci yang sama dimasukkan ke dalam array dengan urutan yang sama seperti pada urutannya. Ini memungkinkan Anda untuk mencapai hasil yang serupa seperti di atas. Sebagai contoh:

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
sumber
3
Faktanya menurut inisialisasi 'pengelompokan' dokumen membuat kamus dengan array sebagai nilai (itu artinya 'pengelompokan', kan?), Dan dalam kasus di atas akan lebih seperti [1: [Person(name: "Franz", position: 1)], 2: [Person(name: "Heinz", position: 2)], 3: [Person(name: "Hans", position: 3)]]. Bukan hasil yang diharapkan, tetapi dapat dipetakan lebih jauh ke kamus datar jika Anda mau.
Varrry
Eureka! Ini droid yang saya cari-cari! Ini sempurna dan sangat menyederhanakan hal-hal dalam kode saya.
Zonker.in.Geneva
Tautan mati ke penginisialisasi. Bisakah Anda memperbarui untuk menggunakan tautan permanen?
Mark A. Donohoe
@MarqueIV Saya memperbaiki tautan yang rusak. Tidak yakin apa yang Anda maksud dengan tautan permanen dalam konteks ini?
Mackie Messer
Permalinks adalah tautan yang digunakan situs web yang tidak akan pernah berubah. Jadi misalnya, alih-alih sesuatu seperti 'www.SomeSite.com/events/today/item1.htm' yang dapat berubah dari hari ke hari, kebanyakan situs akan menyiapkan 'tautan permanen' kedua seperti 'www.SomeSite.com?eventid= 12345 'yang tidak akan pernah berubah, dan karenanya, aman digunakan di tautan. Tidak semua orang melakukannya, tetapi sebagian besar halaman dari situs besar akan menawarkannya.
Mark A. Donohoe
8

Anda dapat menulis penginisialisasi khusus untuk Dictionaryjenis, misalnya dari tupel:

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

dan kemudian gunakan mapuntuk larik Anda dari Person:

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

Bagaimana dengan solusi berbasis 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
        }
    }
}

Beginilah cara Anda menggunakannya:

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.dll
sumber
2

Anda dapat menggunakan fungsi pengurangan. Pertama saya telah membuat penginisialisasi khusus untuk kelas Person

class Person {
  var name:String
  var position:Int

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

Kemudian, saya telah menginisialisasi Array of values

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

Dan terakhir, variabel kamus memiliki hasil:

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

Inilah yang selama ini saya gunakan

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}

Alangkah baiknya jika ada cara untuk menggabungkan 2 baris terakhir sehingga peopleByPositionbisa menjadi let.

Kita bisa membuat ekstensi ke Array yang melakukan itu!

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

Maka kita bisa melakukannya

let peopleByPosition = persons.mapToDict(by: {$0.position})
datinc
sumber
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
sumber
1

Mungkin sesuatu seperti ini?

myArray.forEach({ myDictionary[$0.position] = $0.name })
Varun Goyal
sumber