Swiftでの文字列部分文字列の仕組み


354

私は古いコードの一部を更新してSwift 3で回答していますが、Swift文字列と部分文字列を使用したインデックス作成に到達すると混乱を招きました。

具体的には、次のことを試みました。

let str = "Hello, playground"
let prefixRange = str.startIndex..<str.startIndex.advancedBy(5)
let prefix = str.substringWithRange(prefixRange)

2行目は私に次のエラーを与えていました

タイプ 'String'の値にはメンバー 'substringWithRange'がありません

String現在、次のメソッドがあることがわかります。

str.substring(to: String.Index)
str.substring(from: String.Index)
str.substring(with: Range<String.Index>)

これらは最初は本当に混乱していたので、私はインデックスとレンジをいじり始めました。これは部分文字列のフォローアップの質問と回答です。以下に回答を追加して、それらの使用方法を示します。



または下付き文字列またはサブストリングstackoverflow.com/questions/24092884/...
レオDabus

回答:


831

ここに画像の説明を入力してください

次の例はすべて

var str = "Hello, playground"

スウィフト4

Swift 4で文字列がかなり大幅に見直されました。文字列から部分文字列を取得すると、Substring型ではなく型が返されます。Stringます。どうしてこれなの?文字列はSwiftの値型です。つまり、1つの文字列を使用して新しい文字列を作成する場合は、それをコピーする必要があります。これは安定性には役立ちますが(知らないうちに変更されることはありません)、効率は良くありません。

一方、部分文字列は、元の文字列への参照です。これはそれを説明するドキュメントからの画像です。

コピーは不要なので、使用する方がはるかに効率的です。ただし、100万文字の文字列から10文字の部分文字列を取得したとします。サブストリングはストリングを参照しているため、サブストリングが存在する限り、システムはストリング全体を保持する必要があります。したがって、部分文字列の操作が完了したら、それを文字列に変換します。

let myString = String(mySubstring)

これは上だけで部分文字列をコピーし、古い文字列を保持するメモリをすることができます再利用します。部分文字列(型として)は、短命であることを意味します。

Swift 4のもう1つの大きな改善点は、文字列がコレクションであることです(ここでも)。つまり、コレクションに対して実行できることは何でも、文字列に対しても実行できます(添え字を使用する、文字を反復処理する、フィルターなど)。

次の例は、Swiftで部分文字列を取得する方法を示しています。

部分文字列を取得する

あなたは(例えば、添字または他の多くの方法を使って、文字列から部分文字列を取得することができprefixsuffixsplit)。ただし、範囲のインデックスではString.Indexなく、まだ使用する必要がありIntます。(私の他の答えをください助けが必要な場合。)

文字列の始まり

下付き文字を使用できます(Swift 4の片側範囲に注意してください)。

let index = str.index(str.startIndex, offsetBy: 5)
let mySubstring = str[..<index] // Hello

またはprefix

let index = str.index(str.startIndex, offsetBy: 5)
let mySubstring = str.prefix(upTo: index) // Hello

またはさらに簡単:

let mySubstring = str.prefix(5) // Hello

文字列の終わり

下付き文字の使用:

let index = str.index(str.endIndex, offsetBy: -10)
let mySubstring = str[index...] // playground

またはsuffix

let index = str.index(str.endIndex, offsetBy: -10)
let mySubstring = str.suffix(from: index) // playground

またはさらに簡単:

let mySubstring = str.suffix(10) // playground

を使用するときは、を使用suffix(from: index)して最後から数える必要があったことに注意してください-10。文字列のsuffix(x)最後のx文字のみを使用するを使用する場合、これは必要ありません。

文字列の範囲

ここでも、添え字を使用します。

let start = str.index(str.startIndex, offsetBy: 7)
let end = str.index(str.endIndex, offsetBy: -6)
let range = start..<end

let mySubstring = str[range]  // play

に変換SubstringしていますString

忘れずに、部分文字列を保存する準備ができたらString、古い文字列のメモリをクリーンアップできるように、それをaに変換する必要があります。

let myString = String(mySubstring)

Intインデックス拡張を使用していますか?

Airspeed VelocityとOle BegemannによるSwift 3のStringsIntの記事を読んだ後、ベースのインデックス拡張を使用するのをためらっています。Swift 4では文字列はコレクションですが、Swiftチームは意図的にインデックスを使用していません。まだです。これは、さまざまな数のUnicodeコードポイントで構成されるSwift文字に関係しています。実際のインデックスは、文字列ごとに一意に計算する必要があります。IntString.Index

SwiftチームがString.Index将来的に抽象化する方法を見つけられることを願っています。しかし、彼らができるまで、私は彼らのAPIを使用することを選択しています。文字列操作は単純なIntインデックスルックアップではないことを思い出してください。


9
説明用のThx。まあ当然のアップレート。Appleはこれを過度に複雑にした。部分文字列は、string.substring [from ... to]と同じくらい簡単でなければなりません。
テディ

本当に良い説明。ちょっとしたことを除いてgarbage collected;-) Swiftにはガベージコレクションがないことをここの人々に知ってもらいたいです。
クリスチャンアンカーダンプ

@ChristianAnchorDampf、コメントしてくれてありがとう。ガベージコレクションを出しました。新しい表現はどうですか?
スラグ

なんてすごい答えだね!
davidev

194

私はSwiftの文字列アクセスモデルに本当に不満を感じています。すべてがである必要がありIndexます。私が欲しいのはInt、不器用なインデックスや前進ではなく、を使用して文字列のi番目の文字にアクセスすることです(これは、メジャーリリースごとに変更されます)。だから私はに拡張をしましたString

extension String {
    func index(from: Int) -> Index {
        return self.index(startIndex, offsetBy: from)
    }

    func substring(from: Int) -> String {
        let fromIndex = index(from: from)
        return String(self[fromIndex...])
    }

    func substring(to: Int) -> String {
        let toIndex = index(from: to)
        return String(self[..<toIndex])
    }

    func substring(with r: Range<Int>) -> String {
        let startIndex = index(from: r.lowerBound)
        let endIndex = index(from: r.upperBound)
        return String(self[startIndex..<endIndex])
    }
}

let str = "Hello, playground"
print(str.substring(from: 7))         // playground
print(str.substring(to: 5))           // Hello
print(str.substring(with: 7..<11))    // play

5
文字は複数バイトになる可能があるため、インデックスは非常に便利です。試してみるlet str = "🇨🇭🇩🇪🇺🇸Hello" print(str.substring(to: 2))
vadian 2016

112
はい、文字(つまり、拡張書記素クラスタ)が複数バイトを取る可能性があることを理解しています。文字列の文字にアクセスするために詳細なインデックスアドバンスメソッドを使用する必要があるのは、私の不満です。Swiftチームがコアライブラリにオーバーロードを追加して抽象化できないのはなぜですか。「」と入力するとstr[5]、インデックス5の文字にアクセスする必要があります。その文字がどのように表示されても、そのバイト数が必要です。Swiftは開発者の生産性を重視しているのではありませんか?
異なるコード

6
@RenniePet Appleは問題を認識しており、変更が加えられると思います。GitHubのSwift Evolutionページによると、「Swift 4は、デフォルトでUnicodeの正確性を維持しながら、文字列をより強力で使いやすくすることを目指しています」。あいまいですが、期待を続けましょう
Code Different

3
@CodeDifferentなぜAppleが下付き文字アクセスを追加しなかったのですか?そうすることは悪いことだと人々が理解できるように。基本的に、二重ループになる添え字を使用して0..string.countでiを実行する場合、フードインデックスの下で文字列の各バイトを調べて、次の文字を見つける必要があります。インデックスを使用してループする場合は、文字列を1回だけ反復します。ところで、私はこれを嫌っていますが、それが下付き文字が文字列で迅速に使用できない理由です。
Raimundas Sakalauskas 2017年

4
@RaimundasSakalauskasその議論は私には飛ばない。C#には、Unicodeの正確性と整数の添え字の両方があり、非常に便利です。Swift 1では、Appleは開発者countElement(str)が長さを見つけるために使用することを望んでいました。Swift 3では、Appleは文字列を適合さSequenceせず、str.characters代わりに全員に使用を強制しました。これらの人たちは、変更を加えることを恐れていません。整数の添え字に対する彼らの頑固さは本当に理解しにくい
Code Different

102

Swift 5 Extension:

extension String {
    subscript(_ range: CountableRange<Int>) -> String {
        let start = index(startIndex, offsetBy: max(0, range.lowerBound))
        let end = index(start, offsetBy: min(self.count - range.lowerBound, 
                                             range.upperBound - range.lowerBound))
        return String(self[start..<end])
    }

    subscript(_ range: CountablePartialRangeFrom<Int>) -> String {
        let start = index(startIndex, offsetBy: max(0, range.lowerBound))
         return String(self[start...])
    }
}

使用法:

let s = "hello"
s[0..<3] // "hel"
s[3...]  // "lo"

またはユニコード:

let s = "😎🤣😋"
s[0..<1] // "😎"

2
この拡張機能を投稿していただき、ありがとうございます。私はPythonから来ていると思いますが、Swiftは慣れるのに必要以上に難しいものです。Objective CからSwiftへと別の方向に進む人々にとって、より肯定的な確認があるようです。
user3064009 2018年

1
@レオン私はそれを削除しました。4.1より前のcountself.characters
Lou Zell

1
この特定の拡張機能で注意すべき落とし穴はありますか?なぜAppleはこのようなことをしなかったのですか?
Andz

1
@Andzそれは非常に非効率的です。それは文字列の最初から-2回-始まり、そこからすべての文字を「範囲」まで2回解析する必要があります。
kareman

3
またCountableClosedRange<Int>たとえばを記述したい場合は、をとる拡張機能を追加する必要がありますs[0...2]
クリスフレデリック

24

Swift 4および5:

extension String {
  subscript(_ i: Int) -> String {
    let idx1 = index(startIndex, offsetBy: i)
    let idx2 = index(idx1, offsetBy: 1)
    return String(self[idx1..<idx2])
  }

  subscript (r: Range<Int>) -> String {
    let start = index(startIndex, offsetBy: r.lowerBound)
    let end = index(startIndex, offsetBy: r.upperBound)
    return String(self[start ..< end])
  }

  subscript (r: CountableClosedRange<Int>) -> String {
    let startIndex =  self.index(self.startIndex, offsetBy: r.lowerBound)
    let endIndex = self.index(startIndex, offsetBy: r.upperBound - r.lowerBound)
    return String(self[startIndex...endIndex])
  }
}

それの使い方:

"abcde" [0]-> "a"

"abcde" [0 ... 2]-> "abc"

"abcde" [2 .. <4]-> "cd"


20

スウィフト4

Swift 4ではにString準拠していCollectionます。の代わりにsubstring、ここでを使用する必要があります。subscript.したがって、"play"から単語のみを切り出したい場合は"Hello, playground"、次のようにすることができます。

var str = "Hello, playground"
let start = str.index(str.startIndex, offsetBy: 7)
let end = str.index(str.endIndex, offsetBy: -6)
let result = str[start..<end] // The result is of type Substring

知っておくことは興味深いです。そうすると、のSubstring代わりにが提供されますStringSubstring元の文字列とストレージを共有するため、これは高速で効率的です。ただし、この方法でメモリを共有すると、メモリリークが発生しやすくなります。

これが、元の文字列をクリーンアップしたい場合に、結果を新しい文字列にコピーする必要がある理由です。通常のコンストラクタを使用してこれを行うことができます:

let newString = String(result)

新しいSubstringクラスの詳細については、[Appleのドキュメント]を参照してください。1

したがって、たとえばRangeの結果としてを取得するNSRegularExpression場合は、次の拡張機能を使用できます。

extension String {

    subscript(_ range: NSRange) -> String {
        let start = self.index(self.startIndex, offsetBy: range.lowerBound)
        let end = self.index(self.startIndex, offsetBy: range.upperBound)
        let subString = self[start..<end]
        return String(subString)
    }

}

range.upperBoundが文字列の長さより大きい場合、コードはクラッシュします。また、私はSwiftの添え字に慣れていなかったので、サンプルの使用法も役に立ちました。datePartOnly = "2018-01-04-08:00" [NSMakeRange(0、10)]などを含めることができます。それ以外は、とてもいい答えです、+ 1 :)。
dcp

今日では、この奇妙な事がある: text[Range( nsRange , in: text)!]
Fattie

10

これは、開始インデックスと終了インデックスが提供されたときに、指定された部分文字列の部分文字列を返す関数です。完全なリファレンスについては、以下のリンクをご覧ください。

func substring(string: String, fromIndex: Int, toIndex: Int) -> String? {
    if fromIndex < toIndex && toIndex < string.count /*use string.characters.count for swift3*/{
        let startIndex = string.index(string.startIndex, offsetBy: fromIndex)
        let endIndex = string.index(string.startIndex, offsetBy: toIndex)
        return String(string[startIndex..<endIndex])
    }else{
        return nil
    }
}

これは、文字列操作をすばやく処理するために私が作成したブログ投稿へのリンクです。 Swiftでの文字列操作(Swift 4もカバー)

または、githubでこの要点を確認できます


9

最初の反応は同じでした。私も、すべてのメジャーリリースで構文とオブジェクトがどのように劇的に変化するかに不満を感じていました。

しかし、経験から、最終的にはマルチバイト文字を扱うような「変化」と戦うことの結果に常に苦しんでいることに気付きました。

そこで私は、アップル社のエンジニアたちの努力を認め、尊重し、彼らがこの「恐ろしい」アプローチを思いついたときの彼らの考え方を理解することで私の役割を果たそうと決心しました。

人生を楽にするための単なる回避策である拡張機能を作成する代わりに(私はそれらが間違っている、または高価であるとは言っていません)、Stringがどのように機能するように設計されているかを理解してみませんか。

たとえば、Swift 2.2で動作する次のコードがありました。

let rString = cString.substringToIndex(2)
let gString = (cString.substringFromIndex(2) as NSString).substringToIndex(2)
let bString = (cString.substringFromIndex(4) as NSString).substringToIndex(2)

そして、たとえばSubstringsを使用して同じアプローチを機能させることをやめた後、最終的に同じバージョンの同じコードで終わる双方向コレクションとして文字列を扱う概念を理解しました。

let rString = String(cString.characters.prefix(2))
cString = String(cString.characters.dropFirst(2))
let gString = String(cString.characters.prefix(2))
cString = String(cString.characters.dropFirst(2))
let bString = String(cString.characters.prefix(2))

これが貢献することを願っています...


1
まあ、複雑な問題に対処することは、ソリューションが洗練されているということを意味しません。繰り返しますが、私も問題を理解していますが、Stringクラス全体とそれを処理することは恐ろしいことです。
inexcitus

5

同じ欲求不満、これはそれほど難しいことではありません...

大きなテキストから部分文字列の位置を取得するこの例をコンパイルしました。

//
// Play with finding substrings returning an array of the non-unique words and positions in text
//
//

import UIKit

let Bigstring = "Why is it so hard to find substrings in Swift3"
let searchStrs : Array<String>? = ["Why", "substrings", "Swift3"]

FindSubString(inputStr: Bigstring, subStrings: searchStrs)


func FindSubString(inputStr : String, subStrings: Array<String>?) ->    Array<(String, Int, Int)> {
    var resultArray : Array<(String, Int, Int)> = []
    for i: Int in 0...(subStrings?.count)!-1 {
        if inputStr.contains((subStrings?[i])!) {
            let range: Range<String.Index> = inputStr.range(of: subStrings![i])!
            let lPos = inputStr.distance(from: inputStr.startIndex, to: range.lowerBound)
            let uPos = inputStr.distance(from: inputStr.startIndex, to: range.upperBound)
            let element = ((subStrings?[i])! as String, lPos, uPos)
            resultArray.append(element)
        }
    }
    for words in resultArray {
        print(words)
    }
    return resultArray
}

( "Why"、0、3)( "substrings"、26、36)( "Swift3"、40、46)を返します


3
これは一部のコードですが、swift3での文字列のインデックス付けと部分文字列の動作を実際には説明していません。
Robert

5

私はSwift 3の新人ですがString、類推のために(インデックス)構文を見ると、インデックスは文字列に制約された「ポインター」のようなものであり、Intは独立したオブジェクトとして役立ちます。base + offset構文を使用すると、次のコードでstringからi番目の文字を取得できます。

let s = "abcdefghi"
let i = 2
print (s[s.index(s.startIndex, offsetBy:i)])
// print c

文字列(文字列)構文を使用した文字列の文字(インデックス)の範囲の場合、次のコードでi番目からf番目の文字を取得できます。

let f = 6
print (s[s.index(s.startIndex, offsetBy:i )..<s.index(s.startIndex, offsetBy:f+1 )])
//print cdefg

String.substring(範囲)を使用して文字列から部分文字列(範囲)を取得するには、次のコードを使用して部分文字列を取得できます。

print (s.substring (with:s.index(s.startIndex, offsetBy:i )..<s.index(s.startIndex, offsetBy:f+1 ) ) )
//print cdefg

ノート:

  1. i番目とf番目は0から始まります。

  2. f番目には、サブスクリプションの範囲で.. <(半開演算子)を使用するため、offsetBY:f + 1を使用します。f番目の位置は含まれません。

  3. もちろん、無効なインデックスなどの検証エラーを含める必要があります。


5

スウィフト4+

extension String {
    func take(_ n: Int) -> String {
        guard n >= 0 else {
            fatalError("n should never negative")
        }
        let index = self.index(self.startIndex, offsetBy: min(n, self.count))
        return String(self[..<index])
    }
}

最初のn文字のサブシーケンス、または文字列が短い場合は文字列全体を返します。(インスピレーション:https : //kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/take.html

例:

let text = "Hello, World!"
let substring = text.take(5) //Hello

4

私はかなり機械的な考えです。ここに基本があります...

スイフト4 スイフト5

  let t = "abracadabra"

  let start1 = t.index(t.startIndex, offsetBy:0)
  let   end1 = t.index(t.endIndex, offsetBy:-5)
  let start2 = t.index(t.endIndex, offsetBy:-5)
  let   end2 = t.index(t.endIndex, offsetBy:0)

  let t2 = t[start1 ..< end1]
  let t3 = t[start2 ..< end2]                

  //or a shorter form 

  let t4 = t[..<end1]
  let t5 = t[start2...]

  print("\(t2) \(t3) \(t)")
  print("\(t4) \(t5) \(t)")

  // result:
  // abraca dabra abracadabra

結果は部分文字列です。つまり、元の文字列の一部です。完全に別の文字列を取得するには、例を使用してください

    String(t3)
    String(t4)

これは私が使用するものです:

    let mid = t.index(t.endIndex, offsetBy:-5)
    let firstHalf = t[..<mid]
    let secondHalf = t[mid...]

3

スウィフト4

extension String {
    subscript(_ i: Int) -> String {
        let idx1 = index(startIndex, offsetBy: i)
        let idx2 = index(idx1, offsetBy: 1)
        return String(self[idx1..<idx2])
    }
}

let s = "hello"

s[0]    // h
s[1]    // e
s[2]    // l
s[3]    // l
s[4]    // o

2

このための簡単な拡張機能を作成しました(Swift 3)

extension String {
    func substring(location: Int, length: Int) -> String? {
        guard characters.count >= location + length else { return nil }
        let start = index(startIndex, offsetBy: location)
        let end = index(startIndex, offsetBy: location + length)
        return substring(with: start..<end)
    }
}

2

より一般的な実装を次に示します。

この手法はindex、Swiftの標準を維持するために引き続き使用され、完全なキャラクターを意味します。

extension String
{
    func subString <R> (_ range: R) -> String? where R : RangeExpression, String.Index == R.Bound
    {
        return String(self[range])
    }

    func index(at: Int) -> Index
    {
        return self.index(self.startIndex, offsetBy: at)
    }
}

3番目の文字からサブストリングを作成するには:

let item = "Fred looks funny"
item.subString(item.index(at: 2)...) // "ed looks funny"

キャメルsubStringを使用してString、ではなくを返すことを示しましたSubstring


2

上記に基づいて、非印刷文字をドロップして、非印刷文字で文字列を分割する必要がありました。私は2つの方法を開発しました:

var str = "abc\u{1A}12345sdf"
let range1: Range<String.Index> = str.range(of: "\u{1A}")!
let index1: Int = str.distance(from: str.startIndex, to: range1.lowerBound)
let start = str.index(str.startIndex, offsetBy: index1)
let end = str.index(str.endIndex, offsetBy: -0)
let result = str[start..<end] // The result is of type Substring
let firstStr = str[str.startIndex..<range1.lowerBound]

上記の回答のいくつかを使用してまとめました。

文字列はコレクションなので、次のようにしました。

var fString = String()
for (n,c) in str.enumerated(){

*if c == "\u{1A}" {
    print(fString);
    let lString = str.dropFirst(n + 1)
    print(lString)
    break
   }
 fString += String(c)
}*

私にはどちらがより直感的でした。どれが一番いいですか?どちらもSwift 5で動作することを伝える方法はありません


ご回答有難うございます。Swift 5の文字列に何か違いはありますか?まだ遊んでいる時間がありません。
Suragch

彼らはそう言っていますが、私はそれを調べる機会がありませんでした。
ジェレミーアンドリュース

1

スウィフト4

「サブストリング」(https://developer.apple.com/documentation/swift/substring):

let greeting = "Hi there! It's nice to meet you! 👋"
let endOfSentence = greeting.index(of: "!")!
let firstSentence = greeting[...endOfSentence]
// firstSentence == "Hi there!"

拡張文字列の例:

private typealias HowDoYouLikeThatElonMusk = String
private extension HowDoYouLikeThatElonMusk {

    subscript(_ from: Character?, _ to: Character?, _ include: Bool) -> String? {
        if let _from: Character = from, let _to: Character = to {
            let dynamicSourceForEnd: String = (_from == _to ? String(self.reversed()) : self)
            guard let startOfSentence: String.Index = self.index(of: _from),
                let endOfSentence: String.Index = dynamicSourceForEnd.index(of: _to) else {
                return nil
            }

            let result: String = String(self[startOfSentence...endOfSentence])
            if include == false {
                guard result.count > 2 else {
                        return nil
                }
                return String(result[result.index(result.startIndex, offsetBy: 1)..<result.index(result.endIndex, offsetBy: -1)])
            }
            return result
        } else if let _from: Character = from {
            guard let startOfSentence: String.Index = self.index(of: _from) else {
                return nil
            }
            let result: String = String(self[startOfSentence...])
            if include == false {
                guard result.count > 1 else {
                    return nil
                }
                return String(result[result.index(result.startIndex, offsetBy: 1)...])
            }
            return result
        } else if let _to: Character = to {
            guard let endOfSentence: String.Index = self.index(of: _to) else {
                    return nil
            }
            let result: String = String(self[...endOfSentence])
            if include == false {
                guard result.count > 1 else {
                    return nil
                }
                return String(result[..<result.index(result.endIndex, offsetBy: -1)])
            }
            return result
        }
        return nil
    }
}

拡張文字列の使用例:

let source =                                   ">>>01234..56789<<<"
// include = true
var from =          source["3", nil, true]  //       "34..56789<<<"
var to =            source[nil, "6", true]  // ">>>01234..56"
var fromTo =        source["3", "6", true]  //       "34..56"
let notFound =      source["a", nil, true]  // nil
// include = false
from =              source["3", nil, false] //        "4..56789<<<"
to =                source[nil, "6", false] // ">>>01234..5"
fromTo =            source["3", "6", false] //        "4..5"
let outOfBounds =   source[".", ".", false] // nil

let str = "Hello, playground"
let hello = str[nil, ",", false] // "Hello"

-1

Swift 5
let desiredIndex: Int = 7 let substring = str[String.Index(encodedOffset: desiredIndex)...]
この部分文字列変数は結果を提供します。
単にここでIntがIndexに変換され、文字列を分割できます。エラーが発生しない限り。


2
これは間違っています。文字は1バイト以上で構成される場合があります。ASCIIテキストでのみ機能します。
レオダバス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.