115

给 String 实现一个安全的 subscript 方法 | kemchenj

 6 years ago
source link: https://kemchenj.github.io/2017-10-09/?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

给 String 实现一个安全的 subscript 方法

发表于 2017-10-09 更新于 2022-05-11 本文字数: 2.8k

完整的实现:Gist

最近刚好接触了字符串的切片,原生的 API 各种麻烦,我就试着实现了几个语法糖,目标是:

let str = "Swift-Evolution"
str[3...]   // "ft-Evolution"
str[...3]   // "Swif"
str[..<3]   // "Swi"
str[3...11] // "ft-Evolut"

Swift 4 的字符串原生也是带 subscript 方法的,接收 Range<String.Index> 之类的范围类型作为参数,而我们需要的是使用 Range<Int> 进行调用,那我们只要构造一个过程,让 Range<Int> map 到 Range<String.Index> 就行了。

subscript(_ range: Range<Int>) -> String {
    let newStartIndex = index(startIndex, offsetBy: range.lowerBound)
    let newEndIndex   = index(startIndex, offsetBy: range.upperBound)
    let newRange      = [newStartIndex..<newEndIndex] // Range<String.Index>
    return String(self[newRange])
}

但这种方式其实是不安全的,可能会出现越界导致的崩溃:

str[(-22)...(-11)] // fatalError

我采取的是这样的检验方式,先检验索引值是否在合理的范围内,如果不合理的话,就拉回到最近的边界上,然后再检查它作为一个起始点和终止点是否合理,如果不合理就返回 nil:

private func validIndex(original: Int) -> String.Index {
    switch original {
    case ...startIndex.encodedOffset : return startIndex
    case endIndex.encodedOffset...   : return endIndex
    default                          : return index(startIndex, offsetBy: original)
    }
}
    
private func validStartIndex(original: Int) -> String.Index? {
    guard original <= endIndex.encodedOffset else { return nil }
    return validIndex(original:original)
}
    
private func validEndIndex(original: Int) -> String.Index? {
    guard original >= startIndex.encodedOffset else { return nil }
    return validIndex(original:original)
}

然后前面的那段代码就可以改写成这样:

subscript(_ range: CountableRange<Int>) -> String {
    guard
        let startIndex = validStartIndex(original: range.lowerBound),
        let endIndex   = validEndIndex(original: range.upperBound),
        startIndex < endIndex
    else {
        return ""
    }
    
    return String(self[startIndex..<endIndex])
}

这里的抽象,可以这么理解,字符串是无限长的,平躺在坐标轴上,它只有一小段是有意义的,我们有字符串从开头到结尾的一个范围 A,有要取值的范围 B,两个范围取交集就是我们取值的结果。如果没有交集,那么获取到的就是一个空集:

索引             0
------------------------------------
字符串           Swfit-Evolution
字符串范围       |--------------|
取值范围    |------------|
交集            |=======|        

而实际实现的时候,我们其实需要实现 8 个范围类型 Range / ClosedRange / CountableRange / CountableClosedRange / PartialRangeFrom/PartialRangeThrough / PartialRangeUpTo / CountablePartialRangeFrom

原因是 Swift 的泛型系统还有没完善,需要实现 Condition Conformance 才可以更好地把范围抽象出来,类似于 Countable 的特性是可以通过泛型抽象出来的,而没必要使用那么多的类型,更具体的解释可以看 Ole Begemann 大神对于 Range 类型的解释

如果实现了 Condition Comformance 的话,只要像文章开头说的那样,一个 map 就能解决了,而不需要像现在这样每个类型写一套。

到现在其实还是没有很懂为什么 Swift 要封装出一个 String.Index 的概念?编码吗?

最后献上一段有趣的代码:

extension Collection {
    // could choose to handle or fail on gaps, out-of-order ranges, overlapping ranges etc
    func fields<P: Collection>(at positions: P) -> [String: SubSequence]? 
        where P.Element == (key: String, value: CountableRange<IndexDistance>)
    { }
}

let barcodeSchema: DictionaryLiteral = [
    "name": 2..<22,
    "pnrCode": 23..<30,
    "fromCity": 30..<33,
    "toCity": 33..<36,
    "carrier": 36..<39,
    "flightNumber": 39..<44,
    "day": 45..<47,
    "seatNo": 47..<51,
]

let fields = barcode.fields(at: barcodeSchema)!

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK