如何使用String.substringWithRange?(或者说,在Swift中Ranges是如何工作的?)

274

我还没有弄清如何在Swift中获取字符串的子串:

var str =Hello, playground”
func test(str: String) -> String {
 return str.substringWithRange( /* What goes here? */ )
}
test (str)

我无法在Swift中创建一个范围(Range)。Playground的自动完成功能并不是非常有用 - 它建议如下:

return str.substringWithRange(aRange: Range<String.Index>)

我在 Swift 标准参考库中没有找到任何有用的内容。这是我的另一个猜测:

return str.substringWithRange(Range(0, 1))

还有这个:

let r:Range<String.Index> = Range<String.Index>(start: 0, end: 2)
return str.substringWithRange(r)

我看到了其他答案(在Swift String中查找字符的索引),它们似乎表明由于StringNSString的桥接类型,因此旧的方法应该起作用,但是不清楚如何做 - 例如,这也无法工作(似乎语法无效):

let x = str.substringWithRange(NSMakeRange(0, 3))

有什么想法?


当您说最后一个示例无法正常工作时,您的意思是什么?它会导致错误或返回意外值吗? - 67cherries
请复制rdar:// 17158813请求下标符号表示法http://openradar.appspot.com/radar?id = 6373877630369792 - Rob Napier
33个回答

259
您可以使用 substringWithRange 方法。它接受一个起始和结束的 String.Index 参数。

您可以使用substringWithRange方法。它需要一个起始和结束的String.Index参数。

var str = "Hello, playground"
str.substringWithRange(Range<String.Index>(start: str.startIndex, end: str.endIndex)) //"Hello, playground"

使用 advancedBy(n) 可以改变起始和结束索引。

var str = "Hello, playground"
str.substringWithRange(Range<String.Index>(start: str.startIndex.advancedBy(2), end: str.endIndex.advancedBy(-1))) //"llo, playgroun"

你仍然可以使用带有NSRange的NSString方法,但是必须确保你正在使用像这样的NSString:

let myNSString = str as NSString
myNSString.substringWithRange(NSRange(location: 0, length: 3))

注意:正如JanX2所提到的,这种第二种方法对于Unicode字符串不安全。


5
好的,我会尽力进行翻译。目前为止,如果有必要,我会继续使用NSString来进行桥接。看起来Swift的String还不完整。 - Connor
16
这不安全!你假设字符串和NSString中的索引是可互换的,但实际上并非如此。 字符串中的索引是指Unicode代码点,而NSString中的索引是指UTF-16代码单元!用表情符号试试看。 - JanX2
3
请注意,字符串中的索引不再指代 Unicode 码点。可以将其视为引用用户可感知的字符。 - JanX2
2
那么这里的正确答案是什么?调用advance意味着我们必须遍历整个可能很长的字符串。但是形成一个NSRange并明确使用NSString是危险的?还剩下什么? - matt
1
谢谢,伙计。在只使用Objective-C 4年后转向Swift有点混乱。 - Felipe
显示剩余8条评论

167

Swift 2

简单

let str = "My String"
let subStr = str[str.startIndex.advancedBy(3)...str.startIndex.advancedBy(7)]
//"Strin"

Swift 3

let startIndex = str.index(str.startIndex, offsetBy: 3)
let endIndex = str.index(str.startIndex, offsetBy: 7)

str[startIndex...endIndex]       // "Strin"
str.substring(to: startIndex)    // "My "
str.substring(from: startIndex)  // "String"

Swift 4

Swift 4substring(to:)substring(from:)已被弃用。

String(str[..<startIndex])    // "My "
String(str[startIndex...])    // "String"
String(str[startIndex...endIndex])    // "Strin"

4
这句话很混乱,你必须先找到索引才能使用它,我不知道为什么我要这么长时间才意识到,但感谢您对人类做出的贡献。 - Warpzit
1
在 Swift 4 中,子字符串操作返回 Substring 类型的实例,而不是 String。需要将结果转换为 String 进行长期存储。可以使用以下代码将其转换为 String:let newString = String(substring)。 - phnmnn

46

截至我写作时,没有任何一个扩展是完全兼容 Swift 4.2的,因此这里有一个覆盖了我所能想到的所有需求的扩展:

extension String {
    func substring(from: Int?, to: Int?) -> String {
        if let start = from {
            guard start < self.count else {
                return ""
            }
        }

        if let end = to {
            guard end >= 0 else {
                return ""
            }
        }

        if let start = from, let end = to {
            guard end - start >= 0 else {
                return ""
            }
        }

        let startIndex: String.Index
        if let start = from, start >= 0 {
            startIndex = self.index(self.startIndex, offsetBy: start)
        } else {
            startIndex = self.startIndex
        }

        let endIndex: String.Index
        if let end = to, end >= 0, end < self.count {
            endIndex = self.index(self.startIndex, offsetBy: end + 1)
        } else {
            endIndex = self.endIndex
        }

        return String(self[startIndex ..< endIndex])
    }

    func substring(from: Int) -> String {
        return self.substring(from: from, to: nil)
    }

    func substring(to: Int) -> String {
        return self.substring(from: nil, to: to)
    }

    func substring(from: Int?, length: Int) -> String {
        guard length > 0 else {
            return ""
        }

        let end: Int
        if let start = from, start > 0 {
            end = start + length - 1
        } else {
            end = length - 1
        }

        return self.substring(from: from, to: end)
    }

    func substring(length: Int, to: Int?) -> String {
        guard let end = to, end > 0, length > 0 else {
            return ""
        }

        let start: Int
        if let end = to, end - length > 0 {
            start = end - length + 1
        } else {
            start = 0
        }

        return self.substring(from: start, to: to)
    }
}

然后,您可以使用:

let string = "Hello,World!"

string.substring(from: 1, to: 7)将返回:ello,Wo

string.substring(to: 7)将返回:Hello,Wo

string.substring(from: 3)将返回:lo,World!

string.substring(from: 1, length: 4)将返回:ello

string.substring(length: 4, to: 7)将返回:o,Wo

更新了substring(from: Int?, length: Int)以支持从零开始。


这个处理表情符号和扩展字形集合的方式是怎样的?(我应该自己测试一下,但目前不太方便。) - Suragch
2
@Suragch 它处理得很好:"Hello∝ℵℶ≹".substring(from: 4, to: 14) 给我们的结果是:o∝ℵℶ - winterized
那些看起来像普通字符。不过,试着使用像 ‍‍‍ 或者的表情符号。 - Ky -

44

注意:@airspeedswift在这种方法的权衡方面提出了非常有见地的观点,特别是隐藏的性能影响。字符串并不是简单的东西,到达特定索引可能需要O(n)时间,这意味着使用下标的循环可能会是O(n^2)。你已经被警告了。

您只需要添加一个新的subscript函数,它接受一个范围并使用advancedBy()来移动到您想要的位置:

import Foundation

extension String {
    subscript (r: Range<Int>) -> String {
        get {
            let startIndex = self.startIndex.advancedBy(r.startIndex)
            let endIndex = startIndex.advancedBy(r.endIndex - r.startIndex)

            return self[Range(start: startIndex, end: endIndex)]
        }
    }
}

var s = "Hello, playground"

println(s[0...5]) // ==> "Hello,"
println(s[0..<5]) // ==> "Hello"

(This should definitely be part of the language. Please dupe rdar://17158813)
为了好玩,您还可以将 + 操作符添加到索引中:
func +<T: ForwardIndex>(var index: T, var count: Int) -> T {
  for (; count > 0; --count) {
    index = index.succ()
  }
  return index
}

s.substringWithRange(s.startIndex+2 .. s.startIndex+5)

(我还不确定这个是否应该成为语言的一部分。)

2
出于性能考虑,我们应该尽可能使用 String.Index 而非整数索引进行操作。每次将整数索引转换为 String.Index 都需要处理字符串以查找位置。 - JanX2
11
@AndrewDunn 这不是过早优化。每次从整数索引转换为String.Index的转换都是O(n),而不是O(1)! 请阅读Swift命名空间中关于advance<T>(...)的注释。 - JanX2
1
整个字符串实现都是使用 String.Index 完成的。在这种情况下,它不是那么多的优化,而是保持一切简单明了。到处在 int 和 String.Index 之间来回搞会导致混乱。 - chakrit
尽快修复那些for循环、var函数参数和递增/递减运算符的问题。请参考https://github.com/apple/swift-evolution#accepted-proposals-for-swift-30。 - brimstone
扩展字符串 { 下标 (r: Range<Int>) -> 字符串 { 获取 { //let startIndex = advance(self.startIndex, r.startIndex) let startIndex = self.startIndex.advancedBy(r.startIndex) let endIndex = startIndex.advancedBy(r.endIndex - r.startIndex) 返回自我[范围(开始: startIndex, 结束: endIndex)] } }} // -----------这是用于SWIFT 2.1 - uplearned.com
显示剩余2条评论

31

SWIFT 2.0

简单:

let myString = "full text container"
let substring = myString[myString.startIndex..<myString.startIndex.advancedBy(3)] // prints: ful

SWIFT 3.0

let substring = myString[myString.startIndex..<myString.index(myString.startIndex, offsetBy: 3)] // prints: ful

SWIFT 4.0

子字符串操作返回 Substring 类型的实例,而不是 String。

let substring = myString[myString.startIndex..<myString.index(myString.startIndex, offsetBy: 3)] // prints: ful

// Convert the result to a String for long-term storage.
let newString = String(substring)

4
很遗憾,从Xcode 8 beta 6开始,它与Swift 3不再兼容。 - Ben Morrow
1
对我来说没问题。Xcode 8,Swift 3。在Mac OS Sierra上不是beta版。text[text.startIndex..<text.index(text.startIndex, offsetBy: 2)] - Hedylove

19

一旦找到正确的语法,它比这里的任何答案都简单得多。

我想去掉 [ 和 ]

let myString = "[ABCDEFGHI]"
let startIndex = advance(myString.startIndex, 1) //advance as much as you like
let endIndex = advance(myString.endIndex, -1)
let range = startIndex..<endIndex
let myNewString = myString.substringWithRange( range )

结果将会是 "ABCDEFGHI"。startIndex 和 endIndex 也可以在使用中。

let mySubString = myString.substringFromIndex(startIndex)

等等其他内容!

PS:如备注中所示,Swift 2(随 Xcode 7 和 iOS 9 一起发布)中有一些语法变化!

请查看此页面


是的,我也遇到了调试器多次未显示正确字符串的问题。 - Lars Christoffersen
我有一个字符串,类似于 /Date(147410000000)/。如何获取值而不是指定索引。我需要使用 rangeOfString("(") 和 rangeOfString(")") 来获取值。结果应该是 147410000000。我该怎么做? - iPhone Guy
这并不难: var str = "/Date(147410000000)/." var range = str.rangeOfString("(")!.endIndex..<str.rangeOfString(")")!.startIndex let myNewString = str.substringWithRange(range) - Lars Christoffersen
4
自从 Xcode 7 beta 6 版本以来,advance(myString.endIndex, -1) 的语法已经更改为 myString.endIndex.advancedBy(-1) - l --marc l

16

例如,要在我的全名中找到名字(直到第一个空格):

let name = "Joris Kluivers"

let start = name.startIndex
let end = find(name, " ")

if end {
    let firstName = name[start..end!]
} else {
    // no space found
}

startend在此处均为String.Index类型,用于创建Range<String.Index>并在下标访问器中使用(如果原始字符串中存在空格)。

直接从整数位置创建String.Index很困难,如开篇所述。这是因为在我的名字中,每个字符的字节数相等。但是,其他语言中使用特殊重音的字符可能使用了多个字节(取决于所使用的编码)。那么整数应该指向哪个字节呢?

可以使用succpred方法从现有索引创建新的String.Index,它们会确保跳过正确数量的字节以获取到下一个代码点。但是,在这种情况下,更容易搜索字符串中第一个空格的索引以找到结束索引。


16

Since String is a bridge type for NSString, the "old" methods should work, but it's not clear how - e.g., this doesn't work either (doesn't appear to be valid syntax):

 let x = str.substringWithRange(NSMakeRange(0, 3))
对我而言,你问题中真正有趣的部分是:String被桥接到了NSString,因此大多数NSString方法可以直接在String上使用,您可以自由地并且毫不犹豫地使用它们。例如,以下代码可以像您预期的那样工作:
// delete all spaces from Swift String stateName
stateName = stateName.stringByReplacingOccurrencesOfString(" ", withString:"")

然而,就像经常发生的那样,“我的魔力在起作用,但你却不受影响”。你碰巧选择了一个非常罕见的情况,其中存在着一个相同命名的Swift方法,并且在这种情况下,Swift方法会覆盖Objective-C方法。因此,当你使用 str.substringWithRange 时,Swift认为你指的是Swift方法而不是NSString方法,然后你就会陷入困境,因为Swift方法需要一个 Range<String.Index>,而你不知道如何生成它。

简单的解决方法是通过显式转换来防止Swift这样覆盖:

let x = (str as NSString).substringWithRange(NSMakeRange(0, 3))

请注意,这里不需要进行任何额外的重要工作。 "Cast"并不意味着“转换”; String实际上就是一个NSString。我们只是告诉Swift如何在此代码行的目的下查看此变量。
整个过程中真正奇怪的部分是,导致所有问题的Swift方法未经记录。我不知道它在哪里定义; 它既不在NSString标头中,也不在Swift标头中。

但是,我建议报告一个错误。苹果应该“渔或断钩”- 要么给我们一种构造 Range<String.Index> 的方法,要么让这个未记录的方法走开。 - matt
真是太有用了。尽管文档上说了什么,直到我读了这篇文章,我才明白出了什么问题。现在问题解决了 :) - Jon M

13
简短回答是,目前在Swift中做到这一点真的很困难。我的预感是,苹果公司仍然需要为这类便利方法做大量工作。 String.substringWithRange()期望一个Range<String.Index>参数,据我所知,String.Index类型没有生成器方法。你可以从aString.startIndexaString.endIndex获取String.Index值,并在它们上调用.succ().pred(),但那太疯狂了。
如何在String类上添加一个扩展,以使用传统的Int
extension String {
    subscript (r: Range<Int>) -> String {
        get {
            let subStart = advance(self.startIndex, r.startIndex, self.endIndex)
            let subEnd = advance(subStart, r.endIndex - r.startIndex, self.endIndex)
            return self.substringWithRange(Range(start: subStart, end: subEnd))
        }
    }
    func substring(from: Int) -> String {
        let end = countElements(self)
        return self[from..<end]
    }
    func substring(from: Int, length: Int) -> String {
        let end = from + length
        return self[from..<end]
    }
}

let mobyDick = "Call me Ishmael."
println(mobyDick[8...14])             // Ishmael

let dogString = "This 's name is Patch."
println(dogString[5..<6])               // 
println(dogString[5...5])              // 
println(dogString.substring(5))        // 's name is Patch.
println(dogString.substring(5, length: 1))   // 

更新:Swift测试版4解决了下面的问题!

目前来看,在Swift测试版3及以前的版本中,即使是原生的字符串也存在一些处理Unicode字符的问题。上面的狗图标可以正常显示,但下面的内容则不能:

let harderString = "1:1️⃣"
for character in harderString {
    println(character)
}

输出:

1
:
1
️
⃣

1
很奇怪,核心的Swift类似乎缺少许多基本功能。我想苹果不希望我们不断地引用Foundation类来执行简单的任务。 - bluedevil2k
1
当然。请记住,Swift基本上是在黑暗中开发的,每个人对“简单任务”的定义都不同 - 我敢打赌我们会看到这些问题的快速迭代。 - Nate Cook
@JanX2 - 谢谢,你说得对。与 NSString 的无缝桥接让我以为我只使用了 Swift 本地的 String 方法,因为我没有使用任何 myString as NSString 的调用。 - Nate Cook
现在,Swift本地字符串处理中存在一些Unicode错误:请参见我在末尾的注释。 - Nate Cook
@NateCook 可以请您针对该问题提交一个雷达报告吗? - JanX2
显示剩余2条评论

13

在新的 Xcode 7.0 中使用

//: Playground - noun: a place where people can play

import UIKit

var name = "How do you use String.substringWithRange?"
let range = name.startIndex.advancedBy(0)..<name.startIndex.advancedBy(10)
name.substringWithRange(range)

//OUT:

在此输入图片描述


2
.advancedBy(0) 对于 startIndex 不是必需的。 - Mat0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接