所有的集合类型都有切片操作的默认实现,并且有一个接受Range<Index>作为参数的下标方法。下面的操作等价于list.dropFirst():
let list:List=[1,2,3,4,5]
let onePastStart=list.index(after:list.startIndex)
let firstDropped=list[onePastStart..<list.endIndex]
Array(firstDropped)//[2,3,4,5]
因为像list[somewhere..<list.endIndex](从某个位置到结尾的切片)和list[list.startIndex ..<somewhere](从开头到某个位置的切片)是非常常见的操作,所以在标准库中用一种更容易理解的方式对它们进行了定义:
let firstDropped2=list.suffix(from:onePastStart)
默认情况下,firstDropped不是一个列表,它的类型是Slice<List<String>>。Slice是基于任意集合类型的一个轻量级封装,它的实现看上去会是这样的:
struct Slice<Base:Collection>:Collection{
typealias Index=Base.Index
typealias IndexDistance=Base.IndexDistance
let collection:Base
var startIndex:Index
var endIndex:Index
init(base:Base,bounds:Range<Index>){
collection=base
startIndex=bounds.lowerBound
endIndex=bounds.upperBound
}
func index(after i:Index)->Index{
return collection.index(after:i)
}
subscript(position:Index)->Base.Iterator.Element{
return collection[position]
}
typealias SubSequence=Slice<Base>
subscript(bounds:Range<Base.Index>)->Slice<Base>{
return Slice(base:collection,bounds:bounds)
}
}
除了保存对原集合类型的引用,Slice还存储了切片边界的开始索引和终止索引。所以在List的场合,因为列表本身是由两个索引组成的,所以切片占用的大小也将会是原来列表的两倍:
//一个拥有两个节点(start和end)的列表的大小:
MemoryLayout.size(ofValue:list)//32
//切片的大小是列表的大小再加上子范围的大小
//(两个索引之间的范围。在List的情况下这个范围也是节点)
MemoryLayout.size(ofValue:list.dropFirst())//64
我们可以改善这一点,因为列表可以通过返回自身,但是持有不同的起始索引和终止索引来表示一个子序列,可以用自定义的List方法来进行实现:
extension List{
public subscript(bounds:Range<Index>)->List<Element>{
return List(startIndex:bounds.lowerBound,endIndex:bounds.upperBound)
}
}
在这样的实现下,列表切片也就变成了列表本身,所以它们尺寸依然只有32字节:
let list:List=[1,2,3,4,5]
MemoryLayout.size(ofValue:list.dropFirst())//32
也许除了尺寸的优化,这么做带来的更重要的好处是现在序列和集合的子序列将使用同样的类型,不必再处理另外的类型。比如,我们精心设计的CustomStringConvertible实现现在可以直接用在子序列上,而不必增加额外的代码了。
另一件需要考虑的事情是,对于包括Swift数组和字符串在内的很多可以被切片的容器,它们的切片将和原来的集合共享存储缓冲区。这会带来一个不好的副作用:切片将在它的整个生命周期中持有集合的缓冲区,而不管集合本身是不是已经超过了作用范围。如果你将一个1 GB的文件读入数组或者字符串中,然后获取了它的一个很小的切片,那么整个这1 GB的缓冲区会一直存在于内存中,直到集合和切片都被销毁时才能被释放。这也是Apple在文档中 [11] 特别警告“只应当将切片用作临时计算的目的”的原因。
对List来说,这个问题不是很严重。我们看到,节点是通过ARC来管理的:当切片是仅存的复制时,所有在切片前方的节点都会变成无人引用的状态,这部分内存将得到回收(见图3.4和图3.5)。
图3.4 列表共享和ARC
图3.5 内存回收
不过,切片后方的节点并不会被回收。因为切片的最后一个节点仍然持有着对后方节点的引用(见图3.6)。
图3.6 内存不被回收
切片的索引基本上可以和原来集合的索引互换使用。这不是正式的要求,不过因为标准库中的所有切片类型都是这么做的,所以在我们进行实现的时候,最好也遵循这条规则。
这种模型带来了一个很重要的暗示,那就是即使在使用整数索引时,一个集合的索引也并不需要从0开始。下面是一个数组切片的开始索引和终止索引的例子:
let cities=["New York","Rio","London","Berlin",
"Rome","Beijing","Tokyo","Sydney"]
let slice=cities[2...4]
cities.startIndex//0
cities.endIndex//8
slice.startIndex//2
slice.endIndex//5
在这种情况下,如果不小心访问了slice[0],那么程序将会崩溃。这也是我们应当尽可能始终选择for x in collection的形式,而不去手动地用for index in collection.indices进行索引计算的另一个原因。不过有一个例外:如果在通过集合类型的indices进行迭代时,修改了集合的内容,那么indices所持有的任何对原来集合类型的强引用都会破坏写时复制的性能优化,因为这会造成不必要的复制操作。如果集合的尺寸很大,那么这会对性能造成很大的影响。(不是所有集合的Indices类型都持有对原集合的强引用,不过很多集合都是这么做的。)
要避免这件事情发生,可以将for循环替换为while循环,然后在每次迭代的时候手动增加索引值,这样就不会用到indices属性。在这么做的时候,要记住一定要从collection. startIndex开始进行循环,而不要把0作为开始。
现在我们知道所有的集合都能够进行切片了,下面回顾一下在本章前面实现过的前缀迭代器代码,并写出一个对任意集合都适用的版本:
struct PrefixIterator<Base:Collection>:IteratorProtocol,Sequence{
let base:Base
var offset:Base.Index
init(_base:Base){
self.base=base
self.offset=base.startIndex
}
mutating func next()->Base.SubSequence?{
guard offset!=base.endIndex else{return nil}
base.formIndex(after:&offset)
return base.prefix(upTo:offset)
}
}
通过使迭代器直接满足Sequence,我们可以直接对它使用序列的那些函数,而不用另外定义类型:
let numbers=[1,2,3]
Array(PrefixIterator(numbers))
//[ArraySlice([1]),ArraySlice([1,2]),ArraySlice([1,2,3])]