SwiftのArrayをスライスする

Wednesday, March 16, 2016

最近気づいたのと、あまり日本語での情報?が見当たらなかったので、
SwiftでのArrayのスライスについてまとめてみます。
+でそれを使ったStringのExtensionも。

そもそもスライスって?


配列に対して位置や範囲を指定して、要素を取り出して配列として返す事です。スライスとかスライシングって呼ばれます。
RubyやJavaScriptなんかにも同様にあります。

スライスしてみる


Swiftで配列をスライスして取得する方法はいくつかあります。
今回は下記4つに関してまとめてみます。

  1. range を指定して、array[range]として取り出す
  2. prefix/suffix 関数を使う
  3. dropFirst/dropLast 関数を使う
  4. prefixUpTo/suffixFrom 関数を使う

ちなみにスライスをすると、ArraySlice<Element>という型になるので、元のArray<Element>を使いたい場合は、

array.prefix(2).map { $0 }

といった形で、map関数を使うと元に戻せます。

Rangeを使ってスライスする

Arrayには、

public subscript(bounds: Range<Int>) -> ArraySlice<Element>

のように、 Range を指定する subscript が実装されているので、これを用います。

let array = ["a", "b", "c", "d", "e"]
let s1 = array[0..<1] // ["a"]
let s2 = array[2...4] // ["c", "d", "e"]
let s3 = array[0..<0] // []
let s4 = array[0...5] // error!

こんな感じで、Indexの範囲を指定してスライスすることができます。
注意としては、 s4 のように範囲外が含まれているとエラーになります。

prefix/suffix を使う

prefix は先頭から引数で指定した 要素数 分取り出して返します。
suffix は最後尾から引数で指定した 要素数 分取り出して返します。
ここで注意なのが、引数で与える数です。ここにはIndexではなく、 要素数(length) を与えます。

let array = ["a", "b", "c", "d", "e"]
let s1 = array.prefix(0) // []
var s2 = array.prefix(2) // ["a", "b"]
let s3 = array.prefix(10) // ["a", "b", "c", "d", "e"]

let s4 = array.suffix(0) // []
let s5 = array.suffix(2) // ["d", "e"]
let s6 = array.suffix(10) // ["a", "b", "c", "d", "e"]

このように、指定した要素数分取り出して返します。
Rangeの例と異なるのは、 範囲外を指定しても問題なく動作する 点です。 s3,s6 では明らかに先頭/最後尾から数えて10個分取り出すようにしているので範囲を超えますが、エラーにはなりません。
ただ、引数で指定する数値は 0以上が条件 です。 試しに-1を指定するとエラーになります。
ちなみに、引数を指定しない場合と、 prefix(1) / suffix(1) とした場合は同じです。

array.suffix(1) == array.suffix()  // true

dropFirst/dropLast を使う

今度は prefix/suffix とは逆で、
dropFirst は先頭から引数で指定した 要素数 分切り落として残った部分を返します。 suffix は最後尾から引数で指定した 要素数 分切り落として残った部分を返します。

let array = ["a", "b", "c", "d", "e"]
let s1 = array.dropFirst(0) // ["a", "b", "c", "d", "e"]
var s2 = array.dropFirst(2) // ["c", "d", "e"]
let s3 = array.dropFirst(10) // []

let s4 = array.dropLast(0) // ["a", "b", "c", "d", "e"]
let s5 = array.dropLast(2) // ["a", "b", "c"]
let s6 = array.dropLast(10) // []

こちらも、範囲を越える指定をしても、エラーにはなりません。
ただ、引数で指定する数値は 0以上が条件 です。 試しに-1を指定するとエラーになります。

prefixUpTo/suffixFrom を使う

こちらは、先の prefix/suffix と似ていますが、 範囲外を指定した場合はエラーになります

let array = ["a", "b", "c", "d", "e"]
let s14 = array.prefixUpTo(0)
let s15 = array.prefixUpTo(2)
let s16 = array.prefixUpTo(10) // error!

実装を覗いてみると、

public func prefix(upTo end: Index) -> SubSequence {
    return self[startIndex..<end]
  }

(※Swift3.0のものなので、若干メソッドの定義が異なっています。)
中身は一番最初に紹介した、Rangeを使ったパターンと同じですね。

具体的な使用例


例えば、配列に数を入れつつ、直近の10件だけを常に保持するようにして、古いものから切り落として行く場合に使えたりします。

var nums = [Int]()

(0..<100).forEach {
    nums.append($0)
    nums = nums.suffix(10).map { $0 }
}
print(nums) // [90, 91, 92, 93, 94, 95, 96, 97, 98, 99]

使うものは適切に選ぶ必要があり


範囲外を指定しても問題なく動くものを使うか、範囲外がきたらエラーとして扱いたいか、ケースによって使い分ける必要があります。

prefix/suffix を使ったStringのExtension

  • Stringの最初/最後の文字を取得したい
  • Stringが空の場合は空文字を返したい

といった内容で、 var firstvar lastを定義するとします。

スライスを使わないやり方

extension String {
    var first: String {
        return characters.count == 0 ?
            "" :
            String(characters.first!)
    }
    var last: String {
        return characters.count == 0 ?
            "" :
            String(characters.last!)
    }
}

どうしても要素数のチェックしたり、 first/last を強制的にアンラップするかif letで判定するかが挟まれます。
(String.CharacterViewにはsubscriptは定義されていないので)

スライスを使う場合

extension String {
    var first: String {
        return String(str.characters.prefix(1))
    }
    var last: String {
        return String(str.characters.suffix(1))
    }
}

スッキリ と書けます。
それぞれ要素がない場合は、空の String.CharacterView ([])が返り、それをStringのイニシャライザに渡すと空文字を返してくれるので、スッキリとまとまります。

おまけ

最初のRangeの例で範囲外を指定した場合はエラーになりますが、以下のように組み合わせれば、一応範囲外が指定されても、取れる限りの要素を取得できます。

extension Array {
    func safeRange(range: Range<Int>) -> ArraySlice<Element> {
        return self.dropFirst(range.startIndex).prefix(range.endIndex)
    }
}

let array = ["a", "b", "c", "d", "e"]
array.safeRange(0..<1) // ["a"]
array.safeRange(0...1) // ["a", "b"]
array.safeRange(0..<10) // ["a", "b", "c", "d", "e"]
array.safeRange(99...100) // []
techSwiftTips

clocコマンドでプロジェクト内のファイル数や行数を言語毎に見る

RxSwiftで実行するSchedulerの作り方とお行儀良く扱うためのメモ