オブジェクトの配列をSwiftの辞書にマップします


101

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
}

機能的なアプローチmapでそれを行うためのSwiftによるエレガントな方法はありますかflatMap...または他のモダンなSwiftスタイル


ゲームに少し遅れましたが、[position:name]またはの辞書が必要[position:[name]]ですか?同じ位置に2人いる場合、辞書はループ内で最後に遭遇したものだけを保持します....解決策を見つけようとしている同様の質問がありますが、結果は次のようになります。[1: [p1, p3], 2: [p2]]
Zonker.in.Geneva

1
それらは組み込み関数です。こちらをご覧ください。それは基本的にDictionary(uniqueKeysWithValues: array.map{ ($0.key, $0) })
ハニー

回答:


133

わかりましたが、mapそのループのようにちょうど同じ、あなたが使用することができますので、この良い例ではないreduce代わりに、それが結合して1つの値に回すためにあなたのオブジェクトのそれぞれを取りました:

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 2016

それは実際にはループです、この方法はそれを書くための単純化された方法です、パフォーマンスは通常のループと同じか何とか優れています
Tj3n 2016

2
Kendall、以下の私の答えを参照してください。Swift4を使用すると、ループするよりも速くなる可能性があります。ただし、それを確認するためのベンチマークは行っていません。
possen 2017年

3
これは使用しないでください!!! @KendallHelmstetterGelnerが指摘したように、このアプローチの問題はすべての反復であり、辞書全体を現在の状態でコピーするため、最初のループでは1項目の辞書をコピーします。2回目の反復では、2項目の辞書をコピーします。それは大きなパフォーマンスの低下です!! それを行うためのより良い方法は、配列を取得してその中にすべての項目を追加する便利なinitを辞書に書き込むことです。そうすれば、それはまだ消費者にとってワンライナーですが、それはこれが持っている全体の割り当ての混乱を排除します。さらに、アレイに基づいて事前に割り当てることができます。
マークA.ドノホー

2
パフォーマンスが問題になるとは思いません。Swiftコンパイラは、辞書の古いコピーが使用されていないことを認識し、おそらくそれらを作成しません。独自のコードを最適化する最初のルールを覚えておいてください:「それをしないでください」。コンピュータサイエンスのもう1つの格言は、効率的なアルゴリズムはNが大きい場合にのみ有用であり、Nが大きくなることはほとんどないということです。
バグローフ

149

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)でのみ機能し、名前を付けたときに機能しないように見える理由について、少し混乱しています。編集:気にしないでください、私がまだ結果を返そうとしたので、コンパイラがおそらくトリップしました。
DepartamentoB19年

この回答で不明確なのは、何を$0表しているかです。これは、inout「返される」辞書です。実際には返されませんが、変更することは、そのinout性質のために返されることと同じです。
adamjansch

96

Swift 4以降、これは非常に簡単に実行できます。あり2つの 新しいタプル(キーと値のペア)の配列から辞書を構築する初期化子が。キーが一意であることが保証されている場合は、次の操作を実行できます。

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は、シーケンス要素を辞書にグループ化する3番目の初期化子を追加します。辞書キーはクロージャによって派生します。同じキーを持つ要素は、シーケンスと同じ順序で配列に配置されます。これにより、上記と同様の結果を得ることができます。例えば:

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 2018

ユーレカ!これが私が探していたドロイドです!これは完璧で、私のコードの内容を大幅に簡素化します。
Zonker.in.Geneva

初期化子へのリンク切れ。パーマリンクを使用するように更新していただけますか?
マークA.ドノホー

@MarqueIV壊れたリンクを修正しました。この文脈でパーマリンクで何を指しているのかわかりませんか?
MackieMesser19年

パーマリンクは、ウェブサイトが使用するリンクであり、変更されることはありません。したがって、たとえば、「www.SomeSite.com/events/today/item1.htm」のように日々変化する代わりに、ほとんどのサイトは「www.SomeSite.com?eventid=」のような2番目の「パーマリンク」を設定します。 12345 'は決して変更されないため、リンクで安全に使用できます。すべての人がそれを行うわけではありませんが、大きなサイトのほとんどのページがそれを提供します。
マークA.ドノホ

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
}

2

これは私が使っているものです

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.