Swiftで文字列切り出しを「何文字目から何文字目まで〜」みたいにやろうとした時、切り出し範囲の指定がめんどくさくて、ん?🤔🤔🤔ってなりました。
Javaの文字列型のsubstring
メソッドみたいにIntで指定できればいいのに。。。
今回は混乱したSwiftの文字列切り出しについて整理します。
目次
- 文字列切り出しの表記法
- 内部でなにがどうなってるのか
- Stringからサブスクリプトで文字にアクセスする
- 引数:範囲型(Range<String.Index>)
- 返り値:Substring
- まとめ
1.文字列切り出しの表記法
先頭から任意の文字数の切り出し
let str = "0123456789"
// 先頭から3文字切り出し
str.prefix(3) // -> "012"
ワカル。シンプル。
先頭から任意の文字数の切り出し
let str = "0123456789"
// 末尾から3文字切り出し
str.suffix(3) // -> "789"
ワカル。シンプル。
任意の位置から任意の文字数の切り出し
let str = "0123456789"
// 4文字目(先頭から前に3文字目ずらした文字)の位置を指定
let startIndex = str.index(str.startIndex, offsetBy: 3)
// 7文字目(末尾から後に4文字ずらした文字)の位置を指定
// endIndexは末尾+1文字目なので4文字ずらす
let endIndex = str.index(str.endIndex,offsetBy: -4)
str[startIndex] // -> "3"
str[endIndex] // -> "6"
str[startIndex...endIndex] // -> "3456"
チョットマッテ。キュウニムズイ。
は?なんか最初の文字位置と最後の文字位置指定して最終的に添字みたいなやつでアクセスしてる?は?急になに?は?
雰囲気はふんわりわかる気がしますがこれ毎回ググるはめになるやつですね。。。
これを頑張ってしっかり理解しましょう。
※ここ以降は備忘録的な位置付けが強いので、書き方がわかればいい方は上記までを参照していただければ良いかと思います。
2. 内部でなにがどうなってるのか
最後の「任意の位置から任意の文字数の切り出し」で実際に文字列切り出しを行なっているのはこの部分。
str[startIndex...endIndex] // -> "3456"
この処理を紐解き、なにが起こっているのかを見ていきましょう。
2-1. Stringからサブスクリプトで文字にアクセスする
str[startIndex...endIndex] // -> "3456"
↓補足込みでかくと
これは文字列型に添字でアクセスして文字列を切り出しているように見えます。
コレを理解するために必要知識として、サブスクリプトという概念があります。
(参考:サブスクリプト | Swift言語を学ぶ - Tea Leaves)
Swiftでは構造体やクラス、列挙型に添字式でアクセスするときにはsubscript
キーワードを使って定義することが決まっており、[]で定義されている処理はsubscriptメソッドの部分に書かれています。
ですので、String構造体についてsubscript
キーワードで記述されている処理を見れば、文字列切り出しで何をやっているのか理解できそうです。実際に定義が書かれている部分が以下です。
@inlinable public subscript(r: Range<String.Index>) -> Substring { get }
これを見るに、str[startIndex...endIndex]
で行なっている処理は、実のところsubscript
キーワードを使って定義されており、**「str
文字列に対して、String.Index
の範囲型を引数にして、Substring
型の返り値をgetする」**という処理をしているようです。
おお。なんか理解した感が出てきました。
あとは、引数であるRange<String.Index>
と、返り値であるSubstring
型について何者なのか理解できれば、今後はいちいち理解が曖昧なままググらずに済みそうです。
2-2. 引数:範囲型(Range<String.Index>)
引数であるRange型は範囲型いう、その名の通り値のとりうる範囲を示す型となります。
そのRange型の<>内で示されている、String.Indexは文字列型の位置を示す型であり、String型から指定の位置のCharacter型の文字を取り出すときに使われます。
また、String.Indexを指定するときは、文字列の開始位置を示すstartIndexプロパティや、終了位置を示すendIndexプロパティ、特定の文字位置からのズレを指定することで任意の文字列の場所を取得するindex(_:,offsetBy:)プロパティで取得を行います。例えば、
let startIndex = str.index(str.startIndex, offsetBy: 3)
という処理では、「str文字列の最初の文字"0"を基準に3文字分ずらした文字位置を取ってきてくださいね」という処理をしているわけです。
つまり、Rangeは「文字列の位置(String.Index)を範囲で教えてください」ということで、最終的にstr[startIndex...endIndex]
の引数として与えているstartIndex...endIndex
の部分は切り出したい最初の文字列位置から最後の文字列位置までの範囲を示す必要があるというわけですね。
2-3. 返り値:Substring
よし、ここまでで文字列切り出し処理は、文字列型に対して文字位置範囲を引数に指定してあげることで取得してるのね、理解理解。ってなったと思ったらきました、(初学者に取っては)未知の型Substring
。大人しくString型で返しとけや!!って感じですよね。
このSubstring型は部分文字列といい、切り出した部分だけでなく、元の文字列の全体への参照を保持します。そのため部分文字列を保存すると、文字列データの寿命が長くなり、メモリリークのように見える場合があるとのこと。長い文字列から切り出す場合は、String型にキャストしておいた方が無難なようです。
3. まとめ
subscript(r: Range<String.Index>) -> Substring { get }
で定義される文字列切り出しで行なっている処理は、
「文字列に対して、String.Index
で示された文字位置の範囲型を引数にして、Substring
型の返り値をgetする」
ということをしてるんですね。
文字列切り出し完全に理解しました。