はじめに
次のように、重複要素を持つ Array から順序を維持して重複要素を除去したい場合を考えます。
["a", "x", "b", "x", "c", "x", "a", "x", "d"]
["a", "x", "b", "c", "d"]
NSOrderedSet を使う
Foundation フレームワークには順序付き集合を扱うためのクラス NSOrderedSet が提供されています。これを用いることで、順序を維持したまま重複要素を除去できます。
extension Array {
func uniqued() -> Array {
return NSOrderedSet(array: self).array as! [Element]
}
}
let uniqued = ["a", "x", "b", "x", "c", "x", "a", "x", "d"].uniqued()
dump(uniqued)
▿ 5 elements
- "a"
- "x"
- "b"
- "c"
- "d"
とてもカンタンで、とても便利ですね。
構造体の Array から重複要素を除去する
ところで、NSOrderedSet は Foundation フレームワークで提供されたクラスです。ということは、つまり、Objective-C 互換なオブジェクトでないと使えないのでは?と考えていたのですが、Hashable に適合していれば、NSOrderedSet を利用することができます。
こんなカンジの適当な Hashable な構造体があって、
struct Foo: Hashable {
var foo: Int
var bar: Int
}
適当に重複要素を持つリストを作ります。
let array: [Foo] = (0...99999).map {
Foo(foo: $0 % 2, bar: $0 % 3)
}
すると、たしかに順序を維持して、重複を除去することが確認できました。
dump(array.uniqued())
▿ 6 elements
▿ ***.Foo
- foo: 0
- bar: 0
▿ ***.Foo
- foo: 1
- bar: 1
▿ ***.Foo
- foo: 0
- bar: 2
▿ ***.Foo
- foo: 1
- bar: 0
▿ ***.Foo
- foo: 0
- bar: 1
▿ ***.Foo
- foo: 1
- bar: 2
純粋な Swift の値型にも適用できて、とても便利ですね。
なぜ構造体を Foundation フレームワークで扱えるのか?
NSOrderedSet は Foundation フレームワークで提供されたクラスなので、Objective-C 互換なオブジェクトでないと使えないのでは?と、考えていたのですが、実はこれ自体は正しいです。
Swift では、構造体を暗黙的に Objective-C 互換なオブジェクトに変換する仕組みを提供しています。
値型をいったん Any 型に変換して、更に AnyObject に変換することで、値型から参照型を生成することが可能です。
let foo = (Foo(foo: 0, bar: 1) as Any) as AnyObject
dump(foo)
出力結果から、NSObject のサブクラスであることがわかります。
- ***.Foo(foo: 0, bar: 1) #0
- super: NSObject
また、クラス名は type(of:) 関数を使用することで、 __SwiftValue であることがわかります。
let foo = (Foo(foo: 0, bar: 1) as Any) as AnyObject
dump(type(of: foo))
- __SwiftValue #0
逆に、このオブジェクトに対して、Any → Foo の順番で変換することで、もとの値型として扱うこともできるみたいです。
let foo = (Foo(foo: 0, bar: 1) as Any) as AnyObject
let value = (foo as Any) as! Foo
dump(value)
▿ ***.Foo
- foo: 0
- bar: 1
__SwiftValue と Hashable
__SwiftValue は NSObject のサブクラスなので、isEqual メソッドと hash メソッドを実装しています。
isEqual と hash の実装は、変換前の値型が Hashable かどうか判定して、Hashable な場合はこの == と hash(into:) の実装を呼び出しているように見えました。
swift/SwiftValue.mm at swift-5.2-RELEASE · apple/swift
- https://github.com/apple/swift/blob/swift-5.2-RELEASE/stdlib/public/runtime/SwiftValue.mm#L314#L324
- https://github.com/apple/swift/blob/swift-5.2-RELEASE/stdlib/public/runtime/SwiftValue.mm#L328#L336
この実装のおかげで、Swift の Hashable が、Objective-C 側の世界の一意性やハッシュ値に一致するので NSOrderedSet で重複要素を除去することができるみたいです。
Hashable な構造体はこのように、isEqual、hash を実装した NSObject に変換されるので、NSOrderedSet に限らず、Foundation のコレクションの要素として自由に扱えるみたいです。
たとえば、NSCache<AnyObject, AnyObject> のキーとしても機能することが確認できました。
とても便利なので、使っていきたい機能だと思いました。