字符串子字符串在Swift中如何工作


354

我一直在用Swift 3更新一些旧代码和答案,但是当我进入Swift Strings和Substrings索引时,事情变得混乱了。

具体来说,我正在尝试以下操作:

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

第二行给我以下错误

类型'String'的值没有成员'substringWithRange'

我看到现在String确实具有以下方法:

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

起初这些确实让我感到困惑,所以我开始研究指数和范围。这是子字符串的后续问答。我在下面添加一个答案,以显示它们的用法。



Answers:


831

在此处输入图片说明

以下所有示例均使用

var str = "Hello, playground"

斯威夫特4

在Swift 4中,字符串得到了很大的改进。当您现在从字符串中获得了一些子字符串时,您将得到一个Substring返回的类型,而不是String。为什么是这样?字符串是Swift中的值类型。这意味着,如果您使用一个String来创建一个新的String,则必须将其复制过来。这有利于稳定(在您不知情的情况下,没有其他人会改变它),但不利于效率。

另一方面,子字符串是对原始字符串的引用。这是文档中的图像说明了这一点。

无需复制,因此使用效率更高。但是,假设您从一百万个字符的字符串中得到了十个字符的子字符串。因为子字符串引用了字符串,所以只要子字符串存在,系统就必须保留整个字符串。因此,无论何时完成操作子字符串,都将其转换为字符串。

let myString = String(mySubstring)

这将只复制子字符串,并且可以回收保存旧String的内存。子字符串(作为一种类型)意味着寿命很短。

Swift 4的另一个重大改进是(再次),Strings是Collections。这意味着无论您对集合执行什么操作,都可以对字符串进行操作(使用下标,遍历字符,过滤器等)。

以下示例说明如何在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),这是不必要的,它只接受xString 的最后一个字符。

字符串范围

同样,我们在这里仅使用下标。

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

转换SubstringString

别忘了,当您准备保存子字符串时,应该将其转换为a,String以便可以清理旧字符串的内存。

let myString = String(mySubstring)

使用Int索引扩展名?

Int在阅读了Airspeed Velocity和Ole Begemann 的文章Swift 3中字符串后,我犹豫使用基于索引的扩展名。尽管在Swift 4中,字符串是集合,但是Swift团队故意没有使用Int索引。它仍然是String.Index。这与由不同数量的Unicode代码点组成的Swift字符有关。必须为每个字符串唯一地计算实际索引。

我必须说,我希望Swift小组String.Index将来能找到一种抽象的方法。但是直到他们我选择使用他们的API。它可以帮助我记住String操作不只是简单的Int索引查找。


9
Thx为目的。当之无愧的升职。苹果使这个复杂化了。子字符串应该和string.substring [from ... to]一样容易。
泰迪

真的很好的解释。除了一件小事情garbage collected;-)我希望这里的人们知道Swift中没有垃圾回收。
Christian Anchor Dampf

@ChristianAnchorDampf,感谢您抽出宝贵时间发表评论。我拿出垃圾收集了。新措词如何?
Suragch

先生,真是个了不起的答案!
davidev

194

我真的对Swift的String访问模型感到沮丧:一切都必须是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

110
是的,我知道一个字符(即扩展的字素)可以占用多个字节。我感到沮丧的是为什么我们必须使用详细的索引高级方法来访问字符串的字符。Swift团队为什么不能只向Core库添加一些重载来抽象它。如果输入str[5],我想访问索引5处的字符,无论该字符看起来是多少,或占用多少字节。Swift不仅仅是开发人员的生产力吗?
代码不同

6
@RenniePet我相信苹果公司已经意识到了问题所在,并且即将发生变化。根据GitHub上的Swift Evolution页面:“ Swift 4试图使字符串更强大且更易于使用,同时默认情况下保持Unicode正确性”。含糊不清,但让我们保持希望吧
代码不同

3
@CodeDifferent为什么苹果没有添加下标字符访问权限?让人们知道这是一件坏事。基本上,如果您使用双循环的下标在0..string.count中对i进行处理,则导致幕后索引必须遍历字符串的每个字节以找出下一个字符。如果使用索引循环,则仅对字符串进行一次迭代。顺便说一句,我自己讨厌这个,但这就是下标不能立即在字符串上使用的原因。
Raimundas Sakalauskas

4
@RaimundasSakalauskas,这个论点不是我所为。C#同时具有Unicode正确性和整数下标功能,非常方便。在Swift 1中,Apple希望开发人员使用它countElement(str)来找到长度。在Swift 3中,Apple制作了不符合要求的字符串,Sequence并强迫所有人使用它str.characters。这些家伙不怕做出改变。他们对整数下标的固执真的很难理解
代码不同

102

Swift 5扩展程序:

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"

或unicode:

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

2
更好,谢谢您发布此扩展程序!我认为,来自Python的Swift很难适应。似乎对于从Objective C到Swift朝另一个方向发展的人们来说,还有更多肯定的确认。
user3064009 '18

1
@Leon我刚刚将其删除。在4.1之前,count仅适用于self.characters
Lou Zell,

1
此扩展程序有任何注意事项吗?苹果为什么不做这样的事情?
Andz

1
@Andz效率很低。它从字符串的开头开始-两次-并且必须从那里解析每个字符到“范围”-两次。
kareman


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

迅速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而不是String。由于Substring与原始String共享其存储,因此这是快速而有效的。但是,以这种方式共享内存也很容易导致内存泄漏。

这就是为什么要清除原始String后,应将结果复制到新的String中的原因。您可以使用常规构造函数执行此操作:

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 4)

或者您可以在github上看到这个要点


9

我有相同的初始反应。我也对语法和对象在每个主要版本中的如此巨大的变化感到沮丧。

但是,我从经验中意识到,我最终总是会承受尝试与诸如处理多字节字符的“变化”作斗争的后果,如果您要面向全球受众,这是不可避免的。

因此,我决定承认并尊重Apple工程师所做的努力,并通过理解他们的想法来做出自己的贡献,他们提出了这种“可怕的”方法。

除了创建扩展只是使您的生活更轻松的一种解决方法(我不是说它们是错误的或昂贵的)之外,为什么不弄清楚字符串现在是如何工作的。

例如,我有在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)

在放弃尝试使相同的方法(例如,使用子字符串)起作用之后,我终于理解了将字符串视为双向集合的概念,为此我最终得到了相同版本的该代码:

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中的字符串索引和子字符串如何工作。
罗伯特

5

我是Swift 3的新手,但在使用String(index)语法进行类比时,我认为索引就像是一个约束于字符串的“指针”,而Int可以作为一个独立的对象。使用base + offset语法,然后我们可以使用下面的代码从字符串中获取第i个字符:

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

对于使用String(范围)语法从字符串中获取的一系列字符(索引),我们可以使用以下代码从第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的标准,并暗示完整的Character。

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)
    }
}

要从第三个字符中减去字符串:

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

我用骆驼subString表示它返回a String而不是a Substring


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中的Strings有什么不同吗?我还没有时间玩这个游戏。
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
错了 一个字符可能包含一个或多个字节。它仅适用于ascii文本。
Leo Dabus
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.