在Swift中,当您传递值类型时,请将一个数组说成函数。数组的副本用于要使用的函数。
上面的描述是指“复制”字符串,数组和 字典。您在代码中看到的行为将始终如同a 副本发生了。但是,Swift只执行实际的副本 绝对有必要这样做的场景。 Swift管理所有 重复值以确保最佳性能,您不应该避免 用于尝试抢占此优化的分配。
那么这是否意味着复制实际上仅在修改传递的值类型时进行?
有没有办法证明这实际上是潜在的行为?
为什么这很重要?如果我创建一个大型的不可变数组并希望将其从函数传递给函数,我当然不希望继续复制它。我是否应该在这种情况下使用NSArrray,或者只要我不尝试操作传入的数组,Swift数组是否正常工作?
现在只要我没有使用var或inout明确地使函数中的变量可编辑,那么该函数无论如何都无法修改数组。那还能复制一份吗?假设另一个线程可以在其他地方修改原始数组(只有它是可变的),在调用该函数时制作副本(但只有在传入的数组是可变的时)。因此,如果原始数组是不可变的并且函数不使用var或inout,则Swift创建副本没有任何意义。对?那么Apple对上述短语的意思是什么?
答案 0 :(得分:3)
TL; DR:
那么这是否意味着复制实际上仅在修改传递的值类型时进行?
是的!
有没有办法证明这实际上是潜在的行为?
请参见写时复制优化部分中的第一个示例。
在这种情况下我应该只使用NSArrray还是Swift Array可以正常工作 只要我不尝试操纵Array中传递的内容?
如果您将数组作为inout
进行传递,那么您将具有按引用传递的语义,
因此显然避免了不必要的复制。
如果您将数组作为常规参数传递,
那么写时复制优化将开始进行,您应该不会注意到任何性能下降
同时仍然受益于NSArray
带来的更多类型安全性。
现在,只要我不显式使函数中的变量可编辑 通过使用var或inout,该函数无论如何都无法修改数组。 还能复制吗?
您将获得抽象意义上的“副本”。 实际上,由于写时复制机制,基础存储将被共享, 因此避免不必要的复制。
如果原始数组是不可变的,并且该函数未使用var或inout, Swift创建副本没有任何意义。对吧?
确切地说是写复制机制。
那么Apple上面的短语是什么意思?
从本质上讲,Apple意味着您不必担心复制值类型的“成本”, Swift会在后台为您优化它。
相反,您应该考虑值类型的语义, 这是您分配或将它们用作参数后立即获得一份副本。 Swift的编译器实际生成的是Swift的编译器业务。
Swift确实将数组视为值类型(与引用类型相对), 以及结构,枚举和大多数其他内置类型 (即那些属于标准库而不是Foundation的库)。 在内存级别,这些类型实际上是不可变的普通旧数据对象(POD), 启用有趣的优化。 实际上,它们通常分配在堆栈上,而不是堆 [1] 上, (https://en.wikipedia.org/wiki/Stack-based_memory_allocation)。 这样,CPU可以非常有效地管理它们, 并在函数退出 [2] 时自动重新分配其内存, 不需要任何垃圾收集策略。
只要赋值或作为函数传递,就复制值。 这种语义有很多优点, 例如避免创建意外的别名, 而且还使编译器更容易保证值的生存期 存储在另一个对象中或由闭包捕获。 我们可以考虑管理好旧的C指针来理解原因有多难。
有人可能会认为这是一个错误的策略, 因为它涉及每一次分配变量或调用函数时复制。 但是可能违反直觉, 复制小型字体通常比通过引用便宜很多。 毕竟,指针通常与整数大小相同。
但是,对于大型集合(即数组,集合和字典)而言,关注点是合法的, 和较小的大型结构 [3] 。 但是编译器有一个技巧来处理这些问题,即写时复制(请参阅稍后)。
mutating
结构可以定义mutating
方法,
允许改变结构的字段。
这与值类型仅是不可变的POD并不矛盾,
实际上调用mutating
方法只是一个巨大的语法糖
将变量重新分配为与先前值相同的全新值,
除了已突变的字段。
下面的示例说明了这种语义对等:
struct S {
var foo: Int
var bar: Int
mutating func modify() {
foo = bar
}
}
var s1 = S(foo: 0, bar: 10)
s1.modify()
// The two lines above do the same as the two lines below:
var s2 = S(foo: 0, bar: 10)
s2 = S(foo: s2.bar, bar: s2.bar)
与值类型不同,引用类型本质上是在内存级别指向堆的指针。 它们的语义更接近于基于引用的语言, 例如Java,Python或Javascript。 这意味着它们在分配或传递给函数时不会被复制,它们的地址为。 由于CPU不再能够自动管理这些对象的内存, Swift使用参考计数器在后台处理垃圾收集 (https://en.wikipedia.org/wiki/Reference_counting)。
这种语义具有避免复制的明显优势, 因为所有内容都是通过引用分配或传递的。 缺点是存在意外别名的危险, 就像几乎所有其他基于引用的语言一样。
inout
inout
参数不过是指向期望类型的读写指针。
对于值类型,这意味着该函数不会获取该值的副本,
但是指向这些值的指针
因此函数内部的突变会影响value参数(因此,inout
关键字也将因此而受影响)。
换句话说,这为值类型参数提供了函数上下文中的参考语义:
func f(x: inout [Int]) {
x.append(12)
}
var a = [0]
f(x: &a)
// Prints '[0, 12]'
print(a)
对于引用类型,它将使引用本身可变, 就像传递的参数是对象地址的地址一样:
func f(x: inout NSArray) {
x = [12]
}
var a: NSArray = [0]
f(x: &a)
// Prints '(12)'
print(a)
写时复制(https://en.wikipedia.org/wiki/Copy-on-write)是一种优化技术,它可以 可以避免不必要地复制可变变量, 这是在所有Swift的内置集合(即数组,集合和字典)上实现的。 当您分配数组(或将其传递给函数)时, Swift不会复制所述数组,而实际上使用了引用。 第二个数组突变后,副本将立即进行。 以下代码段(Swift 4.1)可以证明这种行为:
let array1 = [1, 2, 3]
var array2 = array1
// Will print the same address twice.
array1.withUnsafeBytes { print($0.baseAddress!) }
array2.withUnsafeBytes { print($0.baseAddress!) }
array2[0] = 1
// Will print a different address.
array2.withUnsafeBytes { print($0.baseAddress!) }
实际上,array2
不会立即获得array1
的副本,
正如事实所指向的一样。
相反,副本是由array2
的突变触发的。
此优化还发生在结构的更深层, 这意味着,例如,如果您的收藏是由其他收藏组成的, 后者还将受益于写时复制机制, 如以下代码段(Swift 4.1)所示:
var array1 = [[1, 2], [3, 4]]
var array2 = array1
// Will print the same address twice.
array1[1].withUnsafeBytes { print($0.baseAddress!) }
array2[1].withUnsafeBytes { print($0.baseAddress!) }
array2[0] = []
// Will print the same address as before.
array2[1].withUnsafeBytes { print($0.baseAddress!) }
实际上,在Swift中实现写时复制机制相当容易, 因为其某些参考计数器API向用户公开。 技巧包括将引用(例如类实例)包装在结构内, 并在变异之前检查该参考是否被唯一引用。 在这种情况下,可以安全地更改包装的值, 否则应将其复制:
final class Wrapped<T> {
init(value: T) { self.value = value }
var value: T
}
struct CopyOnWrite<T> {
init(value: T) { self.wrapped = Wrapped(value: value) }
var wrapped: Wrapped<T>
var value: T {
get { return wrapped.value }
set {
if isKnownUniquelyReferenced(&wrapped) {
wrapped.value = newValue
} else {
wrapped = Wrapped(value: newValue)
}
}
}
}
var a = CopyOnWrite(value: SomeLargeObject())
// This line doesn't copy anything.
var b = a
但是,这里有一个输入警告!
阅读isKnownUniquelyReferenced
的文档,我们会收到以下警告:
如果作为对象传递的实例同时被多个线程访问, 此函数可能仍返回true。 因此,您只能从变异方法中调用此函数 适当的线程同步。
这意味着上述实现不是线程安全的,
由于我们可能会错误地认为包装的对象可以安全地进行突变,
实际上,这样的突变会破坏另一个线程的不变性。
但这并不意味着Swift的写时复制在多线程程序中固有地存在缺陷。
关键是要了解“同时由多个线程访问”的真正含义。
在我们的示例中,如果在多个线程之间共享CopyOnWrite
的同一实例,则会发生这种情况,
例如作为共享全局变量的一部分。
被包装的对象将具有线程安全的写时复制语义,
但是拥有它的实例将受到数据争夺的影响。
原因是Swift必须建立唯一所有权
正确评估isKnownUniquelyReferenced
[4] ,
如果实例的所有者本身在多个线程之间共享,则无法执行此操作。
Swift旨在减轻程序员的负担 如苹果博客所述,在处理多线程环境时 (https://developer.apple.com/swift/blog/?id=10):
选择值类型而不是引用类型的主要原因之一 是更容易推理代码的能力。 如果您总是得到一个唯一的复制实例, 您可以放心,应用程序的其他部分都不会更改后台数据。 这在多线程环境中特别有用 一个不同的线程可能会在下面改变您的数据。 这会产生难以调试的讨厌的错误。
最终,写时复制机制是一种资源管理优化, 像其他优化技术一样, 在编写代码 [5] 时,您不应该考虑。 相反,应该用更抽象的术语思考 并考虑在赋值或作为参数传递时有效复制的值。
[1] 这仅适用于用作局部变量的值。 用作引用类型(例如,类)的字段的值也存储在堆中。
[2] 可以通过检查产生的LLVM字节码来确认这一点 当处理值类型而不是引用类型时, 但是Swift编译器非常渴望执行恒定的传播, 建立一个最小的例子有点棘手。
[3] Swift不允许结构引用自己, 因为编译器将无法静态计算此类类型的大小。 因此,想到这么大的结构不是很现实 复制它会成为合理的问题。
[4]
顺便说一下,这就是isKnownUniquelyReferenced
接受inout
参数的原因,
因为这是目前Swift建立所有权的方法。
[5] 尽管传递值类型实例的副本应该是安全的, 有一个公开的问题表明当前的实施存在一些问题 (https://bugs.swift.org/browse/SR-6543)。
答案 1 :(得分:1)
我不知道Swift中的每个值类型是否相同,但是对于Array
我很确定它是写时复制的,所以它不会复制它,除非你修改它,正如你所说,如果你把它作为一个常数传递,你就不会冒这个风险。
P.S。在Swift 1.2中,您可以使用新的API在您自己的值类型上实现写时复制