给 String 实现一个安全的 subscript 方法 | kemchenj
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 方法
完整的实现: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)!
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK