Swift의 Dictionary에 객체 배열 매핑


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
}

Swift가 기능적 접근 방식 map, flatMap... 또는 다른 현대적인 Swift 스타일을 사용 하는 우아한 방법이 있습니까?


게임에 조금 늦었지만 [position:name]또는 사전을 원 [position:[name]]하십니까? 같은 위치에 두 사람이있는 경우 사전은 마지막으로 발생한 루프 만 유지합니다 .... 해결책을 찾으려고하는 유사한 질문이 있지만 결과가 다음과 같기를 원합니다.[1: [p1, p3], 2: [p2]]
Zonker.in.Geneva

1
그들의 내장 기능입니다. 를 참조하십시오 여기 . 그것은 기본적이다Dictionary(uniqueKeysWithValues: array.map{ ($0.key, $0) })

답변:


128

Okay 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
이 접근 방식이 마음에 들지만 단순히 사전을로드하는 루프를 만드는 것보다 더 효율적입니까? 모든 항목에 대해이 ... 점점 더 큰 사전의 새로운 변경 가능한 사본을 작성하는 것 같다 때문에 성능에 대해 걱정
켄달 Helmstetter Gelner을

그것은 실제로 루프입니다.이 방법은 그것을 작성하는 단순화 된 방법입니다. 성능은 정상 루프보다 같거나 더 좋습니다
Tj3n

2
Kendall, 아래 내 대답을 참조하십시오. Swift 4를 사용할 때 루핑보다 빠를 수 있습니다.
possen

2
이것을 사용하지 마십시오 !!! @KendallHelmstetterGelner가 지적했듯이이 접근 방식의 문제는 모든 반복이며 현재 상태의 전체 사전을 복사하므로 첫 번째 루프에서 한 항목 사전을 복사합니다. 두 번째 반복에서는 항목이 두 개인 사전을 복사합니다. 그것은 엄청난 성능 저하입니다 !! 더 나은 방법은 사전에 배열을 가져 와서 모든 항목을 추가하는 편리한 초기화를 작성하는 것입니다. 이렇게하면 소비자에게는 여전히 한 줄이지 만 전체 할당 엉망이 제거됩니다. 또한 어레이를 기반으로 사전 할당 할 수 있습니다.
Mark A. Donohoe

1
나는 성능이 문제가 될 것이라고 생각하지 않습니다. Swift 컴파일러는 딕셔너리의 이전 사본이 사용되지 않으며 아마도 생성하지 않을 것임을 인식합니다. 자신의 코드를 최적화하는 첫 번째 규칙을 기억하십시오. "하지 마십시오." 컴퓨터 과학의 또 다른 격언은 효율적인 알고리즘은 N이 크고 N이 거의 크지 않을 때만 유용하다는 것입니다.
bugloaf

136

Swift 4@ Tj3n의 접근 방식 intoreduceIt 버전을 사용하여보다 깨끗하고 효율적으로 할 수 있기 때문에 임시 사전과 반환 값을 제거하여 더 빠르고 쉽게 읽을 수 있습니다.

샘플 코드 설정 :

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

이것은 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

이니셜 라이저에 대한 데드 링크. 영구 링크를 사용하도록 업데이트 할 수 있습니까?
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

축소 기능을 사용할 수 있습니다. 먼저 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}

마지막 두 줄을 결합하는 방법이 있다면 좋을 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
    }
}

1

아마 이런 건가요?

myArray.forEach({ myDictionary[$0.position] = $0.name })
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.