Swift中的ArraySlice是如何工作的?

4
我已经阅读了多篇关于在`Swift`中如何使用ArraySliceArray的文章和帖子。
但是,我找不到它内部是如何工作的?ArraySlice是数组的视图这句话具体指什么?
var arr = [1, 2, 3, 4, 5]
let slice = arr[2...4]

arr.remove(at: 2)
print(slice.startIndex) //2
print(slice.endIndex)   //5
slice[slice.startIndex] //3

在上面的代码中,我从arr中删除了index-2(即3)处的元素。 Index-2也是slicestartIndex。 当我打印slice[slice.startIndex]时,它仍然打印3。
由于ArraySlice没有创建任何额外的存储空间,那么为什么Array中的任何更改都不会反映在ArraySlice中?
这些文章/帖子可以在这里找到:

https://dzone.com/articles/arrayslice-in-swift

https://marcosantadev.com/arrayslice-in-swift/


2
Array和ArraySlice都是值类型。一旦原始数组被改变,切片就会获得自己独立的元素存储空间。 - Martin R
@MartinR 没错,这就是我的意思。但是苹果公司说了另外一番话。在其文档中,它说:“firstHalf和secondHalf切片都不会分配任何新的存储空间。相反,每个切片都提供了一个视图,可以查看absences数组的存储。” - PGDev
1
只有在它们中没有任何一个被改变的情况下才是正确的。(与数组和字符串使用的相同的COW技术。)- 如果您想要实现细节,那么可以查看源代码:https://github.com/apple/swift/blob/master/stdlib/public/core/ArraySlice.swift。 - Martin R
@MartinR那么,如果它创建了自己的存储空间,强引用原始数组有什么用处呢? - PGDev
仅当数组或切片被改变时,才会创建自有存储。例如,考虑递归二分查找方法:将左侧或右侧的一半作为切片传递给递归调用不会创建额外的存储空间。 - Martin R
@MartinR 谢谢你的解释。 - PGDev
2个回答

8

ArrayArraySlice都是值类型,这意味着在复制后它们的值是独立的。

var array = [0, 1, 2, 3, 4, 5]
var slice = array[0..<2]

arrayslice 是独立的值,修改其中一个不会影响另一个:

print(slice) // [0, 1]
array.remove(at: 0)
print(slice) // [0, 1]

那是通过Swift标准库的实现细节来实现的,但可以检查源代码以获取一些想法:在Array.swift#L1241中,我们找到了Array.remove(at:)的实现:
  public mutating func remove(at index: Int) -> Element {
    _precondition(index < endIndex, "Index out of range")
    _precondition(index >= startIndex, "Index out of range")
    _makeUniqueAndReserveCapacityIfNotUnique()

   // ...
  }

使用哪种技术

  @inlinable
  @_semantics("array.make_mutable")
  internal mutating func _makeUniqueAndReserveCapacityIfNotUnique() {
    if _slowPath(!_buffer.isMutableAndUniquelyReferenced()) {
      _copyToNewBuffer(oldCount: _buffer.count)
    }
  }

沿着这条线索,我们在 ArrayBuffer.swift#L107 找到了相关内容。

  /// Returns `true` iff this buffer's storage is uniquely-referenced.
  @inlinable
  internal mutating func isUniquelyReferenced() -> Bool {

    // ...
  }

这还不是完整的实现,但(希望)已经证明了(变异的)remove(at:)方法会在元素存储被共享(通过另一个数组或数组切片)时将其复制到新缓冲区。

我们也可以通过打印元素存储的基地址来验证:

var array = [0, 1, 2, 3, 4, 5]
var slice = array[0..<2]

array.withUnsafeBytes { print($0.baseAddress!) }  // 0x0000000101927190
slice.withUnsafeBytes { print($0.baseAddress!) }  // 0x0000000101927190

array.remove(at: 0)

array.withUnsafeBytes { print($0.baseAddress!) }  // 0x0000000101b05350
slice.withUnsafeBytes { print($0.baseAddress!) }  // 0x0000000101927190

相同的“写时复制”技术被用于数组、字典或字符串的复制,或者如果String和Substring共享存储。因此,只要它们中的任何一个没有被改变,数组切片就会与其原始数组共享元素存储。这仍然是一个有用的特性。以下是一个简单的例子:
let array = [1, 4, 2]
let diffs = zip(array, array[1...]).map(-)
print(diffs) // [-3, 2]
< p > array[1...] 是给定数组的一个视图/切片,而不是实际复制元素。

递归二分查找函数,其中传递了左半部分或右半部分的切片,是另一个应用程序。


2

说明:

数组切片是对底层数组的视图,并保留了该数组。

可能(不确定100%),当您从数组中删除一个元素时,它仍然被保留,并且只是被标记为已删除。

这就是为什么当您打印数组时,您看不到3,但切片仍然显示它的原因。

示例:

class Car : CustomStringConvertible {
    var description: String {

        let objectIdentifier = ObjectIdentifier(self) //To get the memory address

        return "car instance - \(objectIdentifier) "
    }

    deinit {
        print("car deallocated")
    }
}

var carSlice : ArraySlice<Car>?

var cars : [Car]? = [Car(), Car(), Car()]

carSlice = cars?[0...1]


print("Going to make cars nil")

cars = nil

print("cars     = \(cars?.description ?? "nil")")
print("carSlice = \(carSlice?.description ?? "nil")")

print("----------------")

print("Going to make carSlice nil")

carSlice = nil

print("carSlice = \(carSlice?.description ?? "nil")")

输出:

Going to make cars nil
cars     = nil
carSlice = [car instance - ObjectIdentifier(0x000060c00001e7b0) , car instance - ObjectIdentifier(0x000060c00001e7c0) ]
----------------
Going to make carSlice nil
carSlice = nil
car deallocated
car deallocated
car deallocated

苹果文档:

不建议长期存储ArraySlice实例。一个切片持有对更大数组的整个存储的引用,而不仅仅是它表示的部分,即使原始数组的生命周期结束后,切片仍然保留这个引用。长期存储切片可能会延长不再可访问的元素的生命周期,这可能看起来像内存和对象泄漏。

参考:

https://developer.apple.com/documentation/swift/arrayslice


当您从数组中删除元素时,“它仍然被保留并且只是被标记为已删除” - 这是不正确的。在Swift数组中没有“标记为已删除”的概念。 - Martin R
谢谢您的澄清。虽然我不是专家,但我认为在内部必须有一种方法来标记已删除的元素,因为相同的内存位置正在共享。 - user1046037
在你的例子中,Car值类型,因此数组/切片元素是指向实际实例的引用/指针。只有在最后一个引用消失后,实例才会被释放。 - Martin R
感谢 @MartinR 的澄清。 - user1046037
抱歉,我指的是引用类型,而不是值类型。类(和闭包)是引用类型。结构体、枚举和元组是值类型。 - Martin R
谢谢@MartinR,完全不同的话题,你能帮我解决https://stackoverflow.com/questions/50892967/voice-over-pushes-child-uitableviewcontroller-cell-down吗? - user1046037

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