Swiftの部分文字列取得が記述量多すぎ問題。
str[str.index(str.startIndex, offsetBy: 3)..<str.index(str.startIndex, offsetBy: 6)]
// "def"
Pythonの何倍のタイピング量が必要なんですかと。
s[3:6] // "def"
ということで今回はこのあたり、掘り下げてみました。
- まずSwiftの部分文字列取得方法について
- 記述量を短くする工夫
- なんでこんな面倒な仕様なの
- str[3...5]のように記述する
- 便利なライブラリ
といった順番でお伝えします。
まずはSwiftでの部分文字列取得について解説
Swiftで部分文字列を取得する方法は他のプログラミング言語と大きく違うため、ここでつまずく人も多いと思います。
ということで、まずはSwiftのフツーな部分文字列取得について確認しておきます。
文字列str
の3文字目から5文字目までの部分文字列を取得するプログラムは、次のようになります。
let str = "abcdefg"
// 部分文字列の開始インデックスを定義 先頭から3文字目
let startIndex = str.index(str.startIndex, offsetBy: 3)
// 部分文字列の終了インデックスを定義 先頭から5文字目
let endIndex = str.index(str.startIndex, offsetBy: 5)
// Range構造体を作成
let range = startIndex...endIndex
// strの3文字目から5文字目までの部分文字列を取得
let substring = str[range]
// 部分文字列を標準出力
print(substring) // 出力:"def\n"
わかりやすいように、処理ごとに行を分けました。
一般的には、部分文字列の取得は次のように1行でまとめて書かれることも多いです。
SwiftのString
構造体は、文字列位置の指定を、単純な数字ではなくString.Index
構造体で指定する仕様になっています。
そのためstr.index(str.startIndex, offsetBy: 3)
のような感じで文字列の最初のインデックスから3つ後のインデックスを作成する、といった回りくどい記述が必要になります。
工夫して短く記述してみる
先頭や末尾からの部分文字列は、文字位置を数値で指定できます。String.Index
構造体を使う必要はありません。
str.prefix(3) // "abc"
// 末尾2文字の部分文字列
str.suffix(2) // "fg"
// 最初の1文字
str.first // "a"
// 最後の1文字
str.last // "g"
上述の例はいい感じに見えますが、3文字目から5文字目までの部分文字列取得は、やはり数値指定できません。
どうしても数値で指定したい場合は、String
をArray<Character>
に変換して、配列内の要素に対するインデックス指定する方法があります。
String(characterArraySlice) // "def"
この方法は、部分文字列が欲しいだけなのに配列にしていて邪道だし、ぜんぜん解決した感じがしませんよね。
なんでこんな面倒な書き方なのさ
そもそもなぜ、部分文字列を取得するのにString.Index
が必要なんだよって思いますよね。ネットの情報を見ても、不満がある人はやはり多いようです。
Swiftの1文字はCharacter
構造体で表されますが、このCharacter
構造体のサイズは、表したい文字によって可変です。
Character("あ").utf8.count // 3
そのため、数値で指定した何文字目かを指定するよりも、「ズバリこの位置」みたいな情報としてString.Index
を使うことで、不要な計算をしなくて済むようにしているんだと思います。
ただ近年は、どんどんプログラマの負担軽減や学習コスト削減が重視されるべきという流れになっている思うので、文字ごとのサイズなんて実行側で面倒見てくれよって思っちゃいます。
つまり納得いかなーい!
String構造体を拡張するとstr[3…5]のようにアクセスできる
String構造体を拡張して、str[3…5]のような指定でも部分文字列を返すメソッドを定義することで、めちゃんこ便利になります。(まぁ便利ってか今どきのプログラミング言語だと当たり前レベルなんだけどね…)
extension String {
public subscript(r: Range<Int>) -> Substring {
let beginIndex = self.index(self.startIndex, offsetBy: r.lowerBound)
let endIndex = self.index(self.startIndex, offsetBy: r.upperBound - 1)
let range = beginIndex...endIndex
return self[range]
}
}
let str = "abcdefg"
// 数値で部分文字列が取得できるようになる
str[3..<6] // "def"
String.subscript(_:)メソッドはString[ ]のようなアクセスを定義します。
String.subscript(Range<String.Index>)
しか定義されておらず不便だったので、追加でString.subscript(Range<Int>)
も定義しました。
String.subscript(Range<Int>)
は3...5
のようなRange<Int>
型の範囲指定を受け付けて、内部でString.Index
に変換して部分文字列を取得しています。
str[3...5]
のようにアクセスしたい場合は、Stringをさらに拡張子、ClosedRange<Int>
を受け付けるシグネチャを作ります。
extension String {
public subscript(r: ClosedRange<Int>) -> Substring {
let beginIndex = self.index(self.startIndex, offsetBy: r.lowerBound)
let endIndex = self.index(self.startIndex, offsetBy: r.upperBound + 1)
// 先ほど作成したsubscriptメソッドを活用
return self[r.lowerBound ..< (r.upperBound+1)]
}
}
let str = "abcdefg"
// 数値で部分文字列が取得できるようになる
str[3...5] // "def"
本来の記述とこの拡張を使用した記述を比べてみましょう。
str[str.index(str.startIndex, offsetBy: 3)...str.index(str.startIndex, offsetBy: 5)] // "def"
str[str.index(str.startIndex, offsetBy: 3)..<str.index(str.startIndex, offsetBy: 6)] // "def"
// 拡張メソッド定義後
str[3...5] // "def"
str[3..<6] // "def"
なんということでしょう、使い勝手がまるで別次元。
実は多くのSwiftプログラマが、このような独自拡張をそれぞれのプロジェクト内で定義しています。でも、プロジェクトそれぞれに同じようなプログラムを書くのは、なんか違くないですか?
というわけでXCodeからインポートできるライブラリを自作しました。探したけど見つけることができなかったので。
SwiftStringを利用する
作成したライブラリはSwiftStringという名前にしました。
CocoaPodsからインポートできそうな古ーいライブラリでSwiftStringというものが存在していたものの、既にメンテナンスされていなかったので、XCodeのAdd Package Dependenciesからインポート可能版として同じ名前で作成しました。
使用するための手順は次のようになります。
XCodeメニューからFile > Add Package Dependenciesを選択し、表示されたダイアログの右上検索欄に「https://github.com/kendimaru/SwiftString.git
」をコピペします。
するとSwiftStringの説明が出てくるので、「Add Package」をクリックします。
Project NavigatorにPackage Dependenciesが追加され、その中にSwiftStringが表示されます。
プログラムでimport SwiftString
と記述すると、str[3...5]
と記述できるようになります。
playgroundの場合、import SwiftString
としても認識しない場合があります。その場合は、XCodeからFile > Save As Workspaceとして保存すれば、playgroundからも認識されるようになります。
ちなみに他の人が作っている(でも古くてXCodeからのインポートには対応していない)ライブラリは、いろいろな機能が盛り込まれていますが、ケンヂまる作のSwiftStringは、str[3...5]のような記述とstr[3..<6]のような記述をサポートする機能しか作っていません。他の機能はほぼほぼ使われないんじゃないかと思ったので、だったらシンプルな方がいいかなと。
何かリクエストあれば、この記事への返信か、GitHubリポジトリkendimaru/SwiftStringのIssueまでお願いします。