Iganinのブログ

日頃の開発で学んだ知見を中心に記事を書いています。

SwiftにおけるArraySliceについて

はじめに

SwiftではArrayに対して、その配列の部分配列を取得しようとするとArrayではなくArraySliceが返却されます。 なぜArrayでないのか、ArraySliceを使用する理由はなんなのか気になったので調べてみました。

環境設定

以下の環境を使用しています。

  • Xcode10.0
  • Swift4.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も必要だなと考えが変わりました。