在Swift字符串中查找字符的索引

222

是时候承认失败了...

在Objective-C中,我可以使用类似以下的内容:

NSString* str = @"abcdefghi";
[str rangeOfString:@"c"].location; // 2

在Swift中,我看到了类似的东西:

var str = "abcdefghi"
str.rangeOfString("c").startIndex

...但是它只会给我一个 String.Index,我可以用它来对原始字符串进行下标访问,但无法提取位置。

顺便说一句,那个String.Index有一个私有ivar叫做_position,其中包含了正确的值。我只是不知道它如何被公开。

我知道我可以很容易地将其添加到String中。 我更好奇的是,这个新API中我错过了什么。


这是一个 GitHub 项目,其中包含了许多用于 Swift 字符串操作的扩展方法:https://github.com/iamjono/SwiftString - RenniePet
我找到的最佳实现在这里:https://dev59.com/g1wY5IYBdhLWcg3ws5oF#32306142 - Carlos García
你需要区分Unicode代码点和扩展字形簇吗? - Ky -
33个回答

259

你不是唯一一个找不到解决方法的人。

String没有实现RandomAccessIndexType,可能是因为它们允许具有不同字节长度的字符。这就是为什么我们必须使用string.characters.count(在 Swift 1.x 中为countcountElements)来获取字符数的原因。这也适用于位置。 _position可能是原始字节数组中的索引,他们不想暴露出来。 String.Index旨在保护我们免受访问字符中间的字节的影响。

这意味着您获得的任何索引都必须从String.startIndexString.endIndex创建(String.Index实现BidirectionalIndexType)。可以使用successorpredecessor方法创建其他索引。

现在有一组方法(在Swift 1.x中为函数)可帮助我们处理索引:

Swift 4.x

let text = "abc"
let index2 = text.index(text.startIndex, offsetBy: 2) //will call succ 2 times
let lastChar: Character = text[index2] //now we can index!

let characterIndex2 = text.index(text.startIndex, offsetBy: 2)
let lastChar2 = text[characterIndex2] //will do the same as above

let range: Range<String.Index> = text.range(of: "b")!
let index: Int = text.distance(from: text.startIndex, to: range.lowerBound)

Swift 3.0

->

Swift 3.0

let text = "abc"
let index2 = text.index(text.startIndex, offsetBy: 2) //will call succ 2 times
let lastChar: Character = text[index2] //now we can index!

let characterIndex2 = text.characters.index(text.characters.startIndex, offsetBy: 2)
let lastChar2 = text.characters[characterIndex2] //will do the same as above

let range: Range<String.Index> = text.range(of: "b")!
let index: Int = text.distance(from: text.startIndex, to: range.lowerBound)

Swift 2.x

->

Swift 2.x

let text = "abc"
let index2 = text.startIndex.advancedBy(2) //will call succ 2 times
let lastChar: Character = text[index2] //now we can index!
let lastChar2 = text.characters[index2] //will do the same as above

let range: Range<String.Index> = text.rangeOfString("b")!
let index: Int = text.startIndex.distanceTo(range.startIndex) //will call successor/predecessor several times until the indices match

Swift 1.x

let text = "abc"
let index2 = advance(text.startIndex, 2) //will call succ 2 times
let lastChar: Character = text[index2] //now we can index!

let range = text.rangeOfString("b")
let index: Int = distance(text.startIndex, range.startIndex) //will call succ/pred several times

使用 String.Index 处理字符串比较麻烦,但是使用一个包装器通过整数索引(参见 https://dev59.com/RmAg5IYBdhLWcg3wDHU3#25152652)来索引是危险的,因为它隐藏了真实索引的低效性。

请注意,Swift 的索引实现存在问题,即为一个字符串创建的索引/范围不能可靠地用于另一个字符串,例如:

Swift 2.x

let text: String = "abc"
let text2: String = ""

let range = text.rangeOfString("b")!

//can randomly return a bad substring or throw an exception
let substring: String = text2[range]

//the correct solution
let intIndex: Int = text.startIndex.distanceTo(range.startIndex)
let startIndex2 = text2.startIndex.advancedBy(intIndex)
let range2 = startIndex2...startIndex2

let substring: String = text2[range2]

Swift 1.x

的翻译是:

Swift 1.x

let text: String = "abc"
let text2: String = ""

let range = text.rangeOfString("b")

//can randomly return nil or a bad substring 
let substring: String = text2[range] 

//the correct solution
let intIndex: Int = distance(text.startIndex, range.startIndex)    
let startIndex2 = advance(text2.startIndex, intIndex)
let range2 = startIndex2...startIndex2

let substring: String = text2[range2]  

1
尽管这可能有些尴尬,但这似乎是答案。希望那两个范围函数能在最终发布之前的文档中得到说明。 - Matt Wilding
5
每个Collection都有一个typealias IndexType。对于数组,它被定义为Int,对于String,它被定义为String.Index。数组和字符串都可以使用范围(以创建子数组和子字符串)。范围是一种特殊类型的Range<T>。对于字符串,它是Range<String.Index>,对于数组则是Range<Int> - Sulthan
那么对于不止一个字符呢? - User
1
Swift 2.0 中,distance(text.startIndex, range.startIndex) 变成了 text.startIndex.distanceTo(range.startIndex) - superarts.org
1
@devios String,就像Foundation中的NSString一样,有一个名为hasPrefix(_:)的方法。 - Sulthan
显示剩余6条评论

90

Swift 3.0 使得这段代码变得更加冗长:

let string = "Hello.World"
let needle: Character = "."
if let idx = string.characters.index(of: needle) {
    let pos = string.characters.distance(from: string.startIndex, to: idx)
    print("Found \(needle) at position \(pos)")
}
else {
    print("Not found")
}

扩展:

extension String {
    public func index(of char: Character) -> Int? {
        if let idx = characters.index(of: char) {
            return characters.distance(from: startIndex, to: idx)
        }
        return nil
    }
}

Swift 2.0 中,这变得更加容易:

let string = "Hello.World"
let needle: Character = "."
if let idx = string.characters.indexOf(needle) {
    let pos = string.startIndex.distanceTo(idx)
    print("Found \(needle) at position \(pos)")
}
else {
    print("Not found")
}

扩展:

extension String {
    public func indexOfCharacter(char: Character) -> Int? {
        if let idx = self.characters.indexOf(char) {
            return self.startIndex.distanceTo(idx)
        }
        return nil
    }
}

Swift 1.x的实现:

为了获得一个纯Swift的解决方案,可以使用以下代码:

let string = "Hello.World"
let needle: Character = "."
if let idx = find(string, needle) {
    let pos = distance(string.startIndex, idx)
    println("Found \(needle) at position \(pos)")
}
else {
    println("Not found")
}

作为 String 的扩展:

extension String {
    public func indexOfCharacter(char: Character) -> Int? {
        if let idx = find(self, char) {
            return distance(self.startIndex, idx)
        }
        return nil
    }
}

2
字符已过时!! - Shivam Pokhriyal

27

Swift 5.0

public extension String {  
  func indexInt(of char: Character) -> Int? {
    return firstIndex(of: char)?.utf16Offset(in: self)
  }
}

Swift 4.0

public extension String {  
  func indexInt(of char: Character) -> Int? {
    return index(of: char)?.encodedOffset        
  }
}

返回 index(of: element).map { target.distance(from: startIndex, to: $0) } - frogcjn
这是最佳答案。 - Apollo
这个问题的答案应该如此简单,我真不敢相信为了解决这么简单的任务需要那么多的长篇大论。感谢@Vincenso。 - MBH

23
extension String {

    // MARK: - sub String
    func substringToIndex(index:Int) -> String {
        return self.substringToIndex(advance(self.startIndex, index))
    }
    func substringFromIndex(index:Int) -> String {
        return self.substringFromIndex(advance(self.startIndex, index))
    }
    func substringWithRange(range:Range<Int>) -> String {
        let start = advance(self.startIndex, range.startIndex)
        let end = advance(self.startIndex, range.endIndex)
        return self.substringWithRange(start..<end)
    }

    subscript(index:Int) -> Character{
        return self[advance(self.startIndex, index)]
    }
    subscript(range:Range<Int>) -> String {
        let start = advance(self.startIndex, range.startIndex)
            let end = advance(self.startIndex, range.endIndex)
            return self[start..<end]
    }


    // MARK: - replace
    func replaceCharactersInRange(range:Range<Int>, withString: String!) -> String {
        var result:NSMutableString = NSMutableString(string: self)
        result.replaceCharactersInRange(NSRange(range), withString: withString)
        return result
    }
}

7
考虑过这样做,但我认为它会掩盖字符串访问的语义问题。想象一下创建一个访问链表的API,其外观与数组的API完全相同。人们可能会编写极其低效的代码。 - Erik Engheim
在许多情况下,仍然存在仅为utf8的字符包。没有像c库函数那样简单的函数,比如indexOf()、substr()和其他类似的函数,这真是太疯狂了。试图让一切都处理NLS当它不存在时,代表了当前String设计的弱点。实际上,没有理由String不能是一个Character数组,而Character可以记录'字节'的数量并包含这些字节。Character可以是一个能有效地包含Character所有细节的结构体或对象。 - Grwww

16

我已经找到了适用于 Swift2 的解决方案:

var str = "abcdefghi"
let indexForCharacterInString = str.characters.indexOf("c") //returns 2

当str =“abcdefgchi”时,索引将是什么? - Vatsal Shukla

8

我不确定如何从String.Index中提取位置,但如果你愿意回退到一些Objective-C框架,你可以通往Objective-C并像以前一样完成它。

"abcdefghi".bridgeToObjectiveC().rangeOfString("c").location

看起来有些NSString方法还没有被(或者可能不会被)移植到String。Contains也是其中之一。


实际上,访问返回值的位置属性似乎已足够让编译器推断出NSString类型,因此不需要进行bridgeToObjectiveC()调用。我的问题似乎只在之前存在的Swift字符串上调用rangeOfString时才会显现出来。看起来像是API问题... - Matt Wilding
很有趣。我不知道它在那些情况下进行了推断。当它已经是一个字符串时,你总是可以使用桥接。 - Connor

8

这里有一个干净的字符串扩展,可以回答这个问题:

Swift 3:

extension String {
    var length:Int {
        return self.characters.count
    }

    func indexOf(target: String) -> Int? {

        let range = (self as NSString).range(of: target)

        guard range.toRange() != nil else {
            return nil
        }

        return range.location

    }
    func lastIndexOf(target: String) -> Int? {



        let range = (self as NSString).range(of: target, options: NSString.CompareOptions.backwards)

        guard range.toRange() != nil else {
            return nil
        }

        return self.length - range.location - 1

    }
    func contains(s: String) -> Bool {
        return (self.range(of: s) != nil) ? true : false
    }
}

Swift 2.2:

extension String {    
    var length:Int {
        return self.characters.count
    }

    func indexOf(target: String) -> Int? {

        let range = (self as NSString).rangeOfString(target)

        guard range.toRange() != nil else {
            return nil
        }

        return range.location

    }
    func lastIndexOf(target: String) -> Int? {



        let range = (self as NSString).rangeOfString(target, options: NSStringCompareOptions.BackwardsSearch)

        guard range.toRange() != nil else {
            return nil
        }

        return self.length - range.location - 1

    }
    func contains(s: String) -> Bool {
        return (self.rangeOfString(s) != nil) ? true : false
    }
}

7
你可以像这样在单个字符串中查找字符的索引,
extension String {

  func indexes(of character: String) -> [Int] {

    precondition(character.count == 1, "Must be single character")

    return self.enumerated().reduce([]) { partial, element  in
      if String(element.element) == character {
        return partial + [element.offset]
      }
      return partial
    }
  }

}

该结果以[String.Distance]即[Int]的形式呈现,如下:

"apple".indexes(of: "p") // [1, 2]
"element".indexes(of: "e") // [0, 2, 4]
"swift".indexes(of: "j") // []

6

Swift 5

查找子字符串的索引

let str = "abcdecd"
if let range: Range<String.Index> = str.range(of: "cd") {
    let index: Int = str.distance(from: str.startIndex, to: range.lowerBound)
    print("index: ", index) //index: 2
}
else {
    print("substring not found")
}

查找字符的索引

let str = "abcdecd"
if let firstIndex = str.firstIndex(of: "c") {
    let index: Int = str.distance(from: str.startIndex, to: firstIndex)
    print("index: ", index)   //index: 2
}
else {
    print("symbol not found")
}

如果字符重复出现,我们需要两个索引来删除它们,该怎么办? - Marlhex

5
如果你想使用熟悉的NSString,可以明确地声明它:
var someString: NSString = "abcdefghi"

var someRange: NSRange = someString.rangeOfString("c")

我还不确定如何在Swift中实现这个。


1
这肯定可以运行,而且编译器似乎非常积极地为您推断NSString类型。我真的希望有一种纯Swift的方法来做到这一点,因为它似乎是一个足够常见的用例。 - Matt Wilding
是的,我在四处寻找,但是我没有找到。可能是因为它们专注于ObjC不支持的领域,因为它们可以填补这些空白而不会失去太多能力。只是随便想想 :) - Logan

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