SwiftのStringのデフォルトビューはcharacters(CharacterView)として扱って良い気がしてきた

mono 
Swift・iOSコラム
6 min readSep 30, 2016

--

SwiftのStringの文字列表現は4種類に分かれています。

文字列処理したい場合、大抵characters(CharacterView)を使うことになるのですが、一々charactersにアクセスするのも面倒なので、Stringのextensionで便利にしたいと思いました。

とはいえ、わざわざキレイにビューに分けている設計を乱すようなことはしたくなくて、どうするのが良いのかなと考えていました。

SwiftのStringのデフォルトビューはcharacters(CharacterView)として扱って良さそう

挙動的にもこんな感じがしていたのですが、実装的にもそんな感じでした。

public var startIndex: Index { return characters.startIndex }

こんなラップメソッドがあったりします。

public typealias Index = CharacterView.Index

また、StringのIndex型もCharacterViewのIndex型と全く同じ(typealiasは別名与えるだけで型としては全く同じ)だったりしますし。

挙動は、StringとCharacterViewのIndex混在したり適当に操作しても一緒でした(ソースがそうなっているので当たり前な気がしつつ二重チェック)。

Stringを色々便利にしてみる

SwiftのStringのデフォルトビューをcharacters(CharacterView)として扱って本当に良いのかな?と思いつつ、上の結果見る限りはそんな気配なのでやってしまいます( ´・‿・`)

コレクション操作可能にしてみる

デフォルトでは、文字列sに対してこのような操作をするとコンパイルエラーになります。(Swiftの2あたりくらいまでは出来た記憶🤔)

ところが、以下のような宣言を追加するだけ(実装は空で良い)で、これらの操作が出来るようになります。

extension String: BidirectionalCollection, RangeReplaceableCollection {}

count実装は別解として、代わりに以下のようなラップメソッドを生やす方法もありますが、せっかくなので上記の方法で良いかなと思いました。

extension String {
public var count: Int { return characters.count }
}

罠もありそう🤔

ただ、一つ挙動不審な点を見つけました。大抵のコレクション操作は問題無く使えるようになりますが、varで可変宣言した文字列に対して、mutating func removeFirst(_ n: Int)を呼ぶと、実行時エラーになってしまいました。他の類似メソッドは問題無く(mutating func removeLast(_ n: Int)以外)、またcharactersを介してアクセスした場合ももちろん問題無かったです。

var s = "abcde"
// OK
s.dropFirst()
s.dropFirst(1)
s.removeFirst()
s.characters.removeFirst(1)
// 実行時エラー(EXC_BAD_ACCESS)
s.removeFirst(1)

メソッド定義が存在しないのが問題かなと思って以下のように足したら直りました。
ただ、こういう挙動あると大丈夫なのか不安になるのと、一体内部的に何が起こっているのか把握出来ておらず、うーん🤔という感じです(´・ω・`)

文字列に対してRange<Int>によるsubscript操作を可能に

こんな感じにすると、扱いの面倒なIndex型ではなくInt型のRange操作が可能となります。

ポイントは次の2点です。

  • まずCharacterViewがビューの実体ということでそちらに実装を寄せる
  • Stringに対して、charactersをラップするような便利メソッドを生やす

元々Index型になっているのは文字のカウントの扱いの配慮ではなく(その配慮はビューの概念でまかなっている)、文字列アクセスコストの問題なのでそのことを明示するような配慮を施せば良いだろうという内容を以下の記事に書きました。

正直これで良いのかな?という疑問はありつつ、SwiftのStringに対して色々考えた結果、今のところこのように考えがまとまった次第です。

注意点

文字列取り扱いにあたって、UIKitとNSRangeやり取りする場合は注意が必要です。

[追記] Swift 4で解消します🎉

Swift 3でも同じように書ける拡張です💁

— ここまで追記 —

Stackoverflowの解答を元に少し弄りつつ試してみました。

// NSRange → Range<Int>
let r2 = n1.toRange()! // 標準メソッドでの変換は4..<8 となりNG

こういう誤用を防ぐために、Indexでちょっと面倒臭くしてるという意図もあるかもしれません。とはいえ、Range<Int>アクセス出来た方が便利な場面は多いので、こういう罠は認識しつつ今回定義したsubscriptなど使っていこうかなと思っています。

--

--