แมปอาร์เรย์ของวัตถุกับพจนานุกรมใน Swift


95

ฉันมีอาร์เรย์ของ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
}

มีวิธีใดที่สวยงามโดย Swift ในการทำเช่นนั้นด้วยแนวทางการใช้mapงานflatMap... หรือสไตล์ Swift สมัยใหม่อื่น ๆ


เล่นเกมช้าไปหน่อย แต่คุณต้องการพจนานุกรม[position:name]หรือ[position:[name]]? หากคุณมีสองคนในตำแหน่งเดียวกันพจนานุกรมของคุณจะเก็บเฉพาะคนสุดท้ายที่พบในวงของคุณเท่านั้น .... ฉันมีคำถามคล้าย ๆ กันซึ่งฉันกำลังพยายามหาทางแก้ไข แต่ฉันต้องการให้ผลลัพธ์เป็นเช่นนั้น[1: [p1, p3], 2: [p2]]
Zonker.in.Geneva

1
มีฟังก์ชันในตัว ดูที่นี่ . มันเป็นพื้นDictionary(uniqueKeysWithValues: array.map{ ($0.key, $0) })
น้ำผึ้ง

คำตอบ:


129

โอเค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
ฉันชอบแนวทางนี้ แต่มีประสิทธิภาพมากกว่าการสร้างลูปเพื่อโหลดพจนานุกรมหรือไม่? ฉันกังวลเกี่ยวกับประสิทธิภาพเพราะดูเหมือนว่าทุกรายการจะต้องสร้างสำเนาใหม่ของพจนานุกรมที่ใหญ่กว่าเดิม ...
Kendall Helmstetter Gelner

จริงๆแล้วมันเป็นลูปวิธีนี้เป็นวิธีที่ง่ายในการเขียนมันประสิทธิภาพจะเหมือนกันหรือดีกว่าลูปปกติ
Tj3n

2
เคนดอลดูคำตอบของฉันด้านล่างอาจทำให้เร็วกว่าการวนซ้ำเมื่อใช้ Swift 4 แม้ว่าฉันจะไม่ได้เปรียบเทียบเพื่อยืนยันสิ่งนั้น
ขึ้น

2
ห้ามใช้ !!! ดังที่ @KendallHelmstetterGelner ชี้ให้เห็นปัญหาของแนวทางนี้คือการทำซ้ำทุกครั้งโดยจะคัดลอกพจนานุกรมทั้งหมดในสถานะปัจจุบันดังนั้นในลูปแรกจะเป็นการคัดลอกพจนานุกรมแบบรายการเดียว การทำซ้ำครั้งที่สองเป็นการคัดลอกพจนานุกรมสองรายการ เป็นการระบายประสิทธิภาพที่ยิ่งใหญ่ !! วิธีที่ดีกว่าคือเขียน init สะดวกในพจนานุกรมที่ใช้อาร์เรย์ของคุณและเพิ่มรายการทั้งหมดในนั้น ด้วยวิธีนี้มันยังคงเป็นแบบซับเดียวสำหรับผู้บริโภค แต่จะช่วยขจัดความยุ่งเหยิงทั้งหมดที่มีอยู่ นอกจากนี้คุณสามารถจัดสรรล่วงหน้าตามอาร์เรย์ได้
Mark A. Donohoe

1
ฉันไม่คิดว่าประสิทธิภาพจะเป็นปัญหา คอมไพเลอร์ Swift จะรับรู้ว่าไม่มีการใช้พจนานุกรมฉบับเก่าและอาจจะไม่ได้สร้างขึ้นด้วยซ้ำ จำกฎข้อแรกของการเพิ่มประสิทธิภาพโค้ดของคุณเอง: "อย่าทำ" ประโยชน์สูงสุดอีกประการหนึ่งของวิทยาการคอมพิวเตอร์คืออัลกอริทึมที่มีประสิทธิภาพจะมีประโยชน์ก็ต่อเมื่อ N มีขนาดใหญ่และ N แทบจะไม่ใหญ่เลย
bugloaf

138

เนื่องจาก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) เท่านั้นและไม่ใช่เมื่อฉันตั้งชื่อ แก้ไข: ไม่เป็นไรคอมไพเลอร์อาจสะดุดเพราะฉันยังพยายามส่งคืนผลลัพธ์
B

สิ่งที่ไม่ชัดเจนในคำตอบนี้คือสิ่งที่$0แสดงถึง: มันคือinoutพจนานุกรมที่เรากำลัง "กลับมา" - แม้ว่าจะไม่ได้กลับมาจริงๆก็ตามเนื่องจากการปรับเปลี่ยนมันจะเทียบเท่ากับการส่งคืนเนื่องจากความเป็นinout-ness
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"]

สิ่งนี้จะทำงานเป็นห่วงของคุณ หากคุณไม่ต้องการ "เขียนทับ" ค่าและยึดติดกับการแมปแรกคุณสามารถใช้สิ่งนี้:

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

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

Swift 4.2เพิ่มinitializer ตัวที่สามที่จัดกลุ่มองค์ประกอบตามลำดับลงในพจนานุกรม คีย์พจนานุกรมได้มาจากการปิด องค์ประกอบที่มีคีย์เดียวกันจะถูกใส่ลงในอาร์เรย์ในลำดับเดียวกันกับในลำดับ สิ่งนี้ช่วยให้คุณได้ผลลัพธ์ที่คล้ายกันดังข้างต้น ตัวอย่างเช่น:

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
ในความเป็นจริงตามเอกสาร 'การจัดกลุ่ม' initializer สร้างพจนานุกรมที่มีอาร์เรย์เป็นค่า (นั่นหมายถึง 'การจัดกลุ่ม' ใช่ไหม) และในกรณีข้างต้นจะเป็นเช่น[1: [Person(name: "Franz", position: 1)], 2: [Person(name: "Heinz", position: 2)], 3: [Person(name: "Hans", position: 3)]]นั้น ไม่ใช่ผลลัพธ์ที่คาดหวัง แต่สามารถแมปไปยังพจนานุกรมแบบแบนได้ไกลขึ้นหากคุณต้องการ
Varrry

ยูเรก้า! นี่คือหุ่นยนต์ที่ฉันกำลังมองหา! สิ่งนี้สมบูรณ์แบบและทำให้สิ่งต่างๆในโค้ดของฉันง่ายขึ้นมาก
Zonker.in Geneva

ลิงก์ที่ตายแล้วไปยังตัวเริ่มต้น คุณช่วยอัปเดตเพื่อใช้ลิงก์ถาวรได้ไหม
Mark A. Donohoe

@MarqueIV ฉันแก้ไขลิงก์ที่ใช้งานไม่ได้ ไม่แน่ใจว่าคุณกำลังอ้างถึงอะไรกับลิงก์ถาวรในบริบทนี้หรือไม่?
Mackie Messer

ลิงก์ถาวรคือลิงก์ที่ใช้ในเว็บไซต์ที่ไม่มีวันเปลี่ยนแปลง ตัวอย่างเช่นแทนที่จะเป็น "www.SomeSite.com/events/today/item1.htm" ซึ่งอาจมีการเปลี่ยนแปลงในแต่ละวันไซต์ส่วนใหญ่จะตั้งค่า "ลิงก์ถาวร" ที่สองเช่น www.SomeSite.com?eventid= 12345 'ซึ่งจะไม่มีวันเปลี่ยนแปลงดังนั้นจึงปลอดภัยที่จะใช้ในลิงก์ ไม่ใช่ทุกคนที่ทำ แต่หน้าเว็บส่วนใหญ่จากไซต์ใหญ่จะเสนอหน้าเดียว
Mark A. Donohoe

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

คุณสามารถใช้ฟังก์ชันลด ก่อนอื่นฉันได้สร้าง initializer ที่กำหนดไว้สำหรับคลาส 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})


โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.