Swift の ArraySlice について
部分配列を取ると Array ではなく ArraySlice が返る理由。要素をコピーせず元の配列への view を返しているためです。
- 公開日
この記事は別サイトにも掲載しています。 https://iganin.hatenablog.com/entry/2019/02/15/012024
はじめに
Swift では Array に対して、その配列の部分配列を取得しようとすると Array ではなく ArraySlice が返却されます。 なぜ Array でないのか、ArraySlice を使用する理由はなんなのか気になったので調べてみました。
環境設定
以下の環境を使用しています。
- Xcode 10.0
- Swift 4.2
ArraySlice について
Apple のドキュメントを確認してみます。
The ArraySlice type makes it fast and efficient for you to perform operations on sections of a larger array. Instead of copying over the elements of a slice to new storage, an ArraySlice instance presents a view onto the storage of a larger array. And because ArraySlice presents the same interface as Array, you can generally perform the same operations on a slice as you could on the original array.
For more information about using arrays, see Array and ContiguousArray, with which ArraySlice shares most properties and methods.
ArraySlice - Swift Standard Library | Apple Developer Documentation
重要な部分をかいつまんでみてみると、以下のようになります。
新しい領域を確保して Array の要素をコピーするのではなく、ArraySlice は Array への view を表します。 また ArraySlice は Array と同様なインターフェースを備えているため Array に実施するのと同様な操作を ArraySlice に行うことができます。
つまり ArraySlice は新規でメモリ領域の確保は行わず、元の配列を参照しながら始点の index と Count を保持していると考えられます。
コードでの確認
上記の確認のため実際にポインタのアドレスを見てみました。 わかりやすさのために prefix を使用しています。ポインタを確認すると、同一であることがわかります。 したがって、a とその部分配列である b は同一のアドレスをさしていることがわかります。
let a = [1, 2, 3, 4, 5]
var b = a.prefix(3)
print(UnsafePointer(a))
b.withUnsafeBufferPointer { (pointer) -> Void in
print(pointer)
}
0x00006000011fcac0
UnsafeBufferPointer(start: 0x00006000011fcac0, count: 3)
値の代入を行った場合にどうなるかも確認してみました。 参照のみ保持している場合は元の Array の値まで変わってしまうのでしょうか。 結果としては下記のように値を変更したタイミングで新規にメモリ領域が確保されました。
let a = [1, 2, 3, 4, 5]
var b = a.prefix(3)
print(UnsafePointer(a))
b.withUnsafeBufferPointer { (pointer) -> Void in
print(pointer)
}
b[2] = 9
b.withUnsafeBufferPointer { (pointer) -> Void in
print(pointer)
}
0x00006000010e0de0
UnsafeBufferPointer(start: 0x00006000010e0de0, count: 3)
UnsafeBufferPointer(start: 0x00006000026f2620, count: 3)
この動きは Struct におけるコピーオンライトと同様の動きになります。 つまり、変数への格納時ではなく値が変わった際にメモリ領域の確保が行われます。
let a = [1, 2, 3, 4, 5]
var c = a
print(UnsafePointer(a))
print(UnsafePointer(c))
c[0] = 1
print(UnsafePointer(c))
0x0000600000622500
0x0000600000622500
0x00006000006360a0
なぜ ArraySlice を使うのかの考え
一番の理由は単純にパフォーマンスではないかと思います。 あたらしいメモリ領域を確保し、そこに Array の要素をコピーするよりは元のデータの参照を保持し、そのどの部分配列を見れば良いかというデータのみを持つ方がパフォーマンスが良いのではないだろうかと直感的に考えます。
終わりに
ArraySlice で返ってくると Array 型に変換しなければ Array で定義してあるメソッドの引数に渡すこと等ができず、正直なところ Array で返してくれればいいのになと思っていたのですが、調べてみて ArraySlice も必要だなと考えが変わりました。