这里每天分享一个 iOS 的新知识,快来关注我吧
前言
swift 中有一些比较奇怪的设计,其中切片的索引问题就经常被诟病,今天来详细讲讲。
首先来猜测以下代码输出什么内容:
css
var arr = [1, 2, 3, 4]
var subArr = arr[1..<3]
print(subArr[0])
切片索引并非都从 0 开始
公布结果,以上代码会发生越界崩溃。
有些人看到这个结果会比较奇怪,subArr = arr[1..<3]
的结果明明是 [2, 3],那 subArr[0]
应该是 2 啊,为什么会越界呢?
其实通过 arr[1..<3]
这种方法取得的是一个 ArraySlice<Element>
类型,也叫数组切片。
看下这个函数的声明:
swift
public subscript(bounds: Range<Int>) -> ArraySlice<Element>
按照苹果官方文档的说法,这个 ArraySlice
和原数组共享内存,同时也共享索引,这样做的目的一方面是能够节省内存,另一方面能够提高大数组的操作效率。
因此我们上边的例子中 subArr
有效的索引应该为 1、2(共享父数组索引)。这点可以通过打印索引看出来:
less
var arr = [1, 2, 3, 4]
var subArr = arr[1..<3]
// 打印索引
print(subArr.indices) // 打印 1..<3
得出这个结论后,我们再尝试获取 subArr 的第一个元素,应该直接取下标 1:
css
var arr = [1, 2, 3, 4]
var subArr = arr[1..<3]
print(subArr[1]) // 打印:2
如何解决这个隐患
这种设计难以用正常思维理解,我查看了一些论坛中的讨论,关于这个设计也不太容易修改掉,因为在集合及其子序列之间共享索引是 Swift 集合算法设计的重要组成部分(说白了就是积重难返了)。
要避免这个坑也有一些方法,比如,我们可以在 ArraySlice
的扩展中写一个新的下标方法来做一层保护(防止崩溃):
swift
extension ArraySlice {
subscript(`safe` index: Index) -> Element? {
if indices.contains(index) {
return self[index]
} else {
return nil
}
}
}
此类方法在之前的文章防止数组越界中也提到过。
其实苹果官方有提到,要安全地引用切片的开始和结束索引,始终使用 startIndex
和 endIndex
属性而不是特定值 ,也就是说,索引的开始位置不一定为 0,而是 startIndex
,因此也可以在扩展中增加另一个方法,兼容这种情况:
swift
extension ArraySlice {
subscript(safeOffset offset: Int) -> Element? {
let index = startIndex + offset
return indices.contains(index) ? self[index] : nil
}
}
这样的话,外界就可以默认切片的索引是从 0 开始的了,使用的时候:
ini
var arr = [1, 2, 3, 4]
var subArr = arr[1..<3]
if let first = subArr[safeOffset: 0] {
print(first) // 打印:2
}
这里每天分享一个 iOS 的新知识,快来关注我吧
本文同步自微信公众号 "iOS新知",每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!