是否存在Swift数组赋值不一致的原因(既不是引用也不是深拷贝)?

时间:2014-06-06 11:41:14

标签: arrays swift

我正在阅读文档,而且我一直在为语言的一些设计决策摇头。但真正让我困惑的是如何处理数组。

我赶紧跑到操场上试了一下。你也可以尝试一下。所以第一个例子:

var a = [1, 2, 3]
var b = a
a[1] = 42
a
b

此处ab都是[1, 42, 3],我可以接受。引用了数组 - 好的!

现在看这个例子:

var c = [1, 2, 3]
var d = c
c.append(42)
c
d

c[1, 2, 3, 42]d[1, 2, 3]。也就是说,d看到了最后一个示例中的更改,但在此示例中没有看到它。文档说的是因为长度发生了变化。

现在,这个怎么样:

var e = [1, 2, 3]
var f = e
e[0..2] = [4, 5]
e
f

e[4, 5, 3],很酷。有一个多索引替换很好,但f仍然没有看到更改,即使长度没有改变。

总而言之,如果更改1个元素,对数组的常见引用会看到更改,但如果更改多个元素或附加项目,则会生成副本。

这对我来说似乎是一个非常糟糕的设计。我是否正确地思考这个?有没有理由我不明白为什么数组应该这样做?

编辑:数组已更改,现在具有值语义。更加理智!

10 个答案:

答案 0 :(得分:109)

请注意,数组语义和语法在Xcode beta 3版本blog post)中已更改,因此该问题不再适用。以下答案适用于beta 2:


出于性能原因。基本上,他们试图避免复制数组,只要他们可以(并声称“类似C的性能”)。引用语言book

  

对于数组,只有在执行可能会修改数组长度的操作时才会进行复制。这包括追加,插入或删除项目,或使用范围下标来替换数组中的一系列项目。

我同意这有点令人困惑,但至少有一个清晰而简单的描述它是如何运作的。

该部分还包括有关如何确保唯一引用数组,如何强制复制数组以及如何检查两个数组是否共享存储的信息。

答案 1 :(得分:25)

From the official documentation of the Swift language

  

请注意,使用下标语法设置新值时不会复制数组,因为使用下标语法设置单个值无法更改数组的长度。但是,如果将新项追加到数组,则执行修改数组的长度。这会提示Swift在您追加新值的位置创建数组的新副本。从此以后,a是一个独立的,独立的数组副本.....

阅读本文档中的数组的分配和复制行为的整个部分。你会发现替换数组中的一系列项目时,数组会为所有项目获取自身的副本。

答案 2 :(得分:20)

Xcode 6 beta 3的行为已经改变。数组不再是引用类型,并且具有写时复制机制,这意味着只要您更改数组的内容,一个或另一个变量,数组将被复制,只有一个副本将被更改。


旧答案:

正如其他人所指出的那样,Swift试图在可能的情况下避免复制数组,包括一次changing values for single indexes

如果要确保数组变量(!)是唯一的,即不与其他变量共享,则可以调用unshare方法。这会复制数组,除非它只有一个引用。当然你也可以调用copy方法,它总是复制,但 unshare 是首选,以确保没有其他变量保留在同一个数组上。

var a = [1, 2, 3]
var b = a
b.unshare()
a[1] = 42
a               // [1, 42, 3]
b               // [1, 2, 3]

答案 3 :(得分:12)

该行为与.NET中的Array.Resize方法非常相似。要了解发生了什么,查看C,C ++,Java,C#和Swift中.标记的历史记录可能会有所帮助。

在C中,结构只不过是变量的集合。将.应用于结构类型的变量将访问存储在结构中的变量。指向对象的指针不包含变量的聚合,但标识它们。如果有一个标识结构的指针,->运算符可用于访问存储在指针标识的结构中的变量。

在C ++中,结构和类不仅聚合变量,还可以将代码附加到它们。使用.调用方法将对变量请求该方法对变量本身的内容执行 ;在标识对象的变量上使用->将要求该方法对变量所识别的对象采取行动。

在Java中,所有自定义变量类型只是标识对象,并且对变量调用方法将告诉方法变量标识了哪个对象。变量不能直接保存任何类型的复合数据类型,也没有任何方法可以通过它来访问调用它的变量。这些限制虽然在语义上有所限制,但却大大简化了运行时,并促进了字节码验证;在市场对这些问题敏感的时候,这种简化减少了Java的资源开销,从而帮助它在市场中获得了动力。它们还意味着不需要与C或C ++中使用的.等效的令牌。尽管Java可以像C和C ++一样使用->,但创建者选择使用单字符.,因为它不需要用于任何其他目的。

在C#和其他.NET语言中,变量可以直接识别对象或保存复合数据类型。当在复合数据类型的变量上使用时,.会对变量的内容起作用;当在引用类型的变量上使用时,.会对它所识别的对象起作用。对于某些类型的操作,语义区别并不是特别重要,但对于其他操作则是如此。最有问题的情况是在只读变量上调用复合数据类型的方法,该方法将修改调用它的变量。如果尝试在只读值或变量上调用方法,则编译器通常会复制变量,让方法对此进行操作,并丢弃该变量。对于只读取变量的方法,这通常是安全的,但对于写入变量的方法则不安全。不幸的是,。目前还没有任何方法可以指出哪种方法可以安全地用于这种替代,哪种方法可以用。

在Swift中,聚合上的方法可以明确地表明它们是否会修改调用它们的变量,编译器将禁止在只读变量上使用变异方法(而不是让它们改变变量的临时副本)然后将被丢弃)。由于这种区别,使用.标记来调用修改调用它们的变量的方法在Swift中比在.NET中更安全。不幸的是,同一个.令牌用于此目的,以便对由变量标识的外部对象进行操作意味着混淆的可能性仍然存在。

如果有时间机器并且回到C#和/或Swift的创建,那么可以通过让语言使用.->令牌来追溯性地避免围绕此类问题的混淆。一种更接近C ++用法的时尚。聚合和引用类型的方法都可以使用.对调用它们的变量进行操作,->值进行操作 (对于复合材料)或由此确定的东西(用于参考类型)。然而,这两种语言都不是这样设计的。

在C#中,修改调用它的变量的方法的通常做法是将变量作为ref参数传递给方法。因此,当Array.Resize(ref someArray, 23);标识20个元素的数组时调用someArray将导致someArray识别包含23个元素的新数组,而不会影响原始数组。使用ref表明应该期望该方法修改调用它的变量。在许多情况下,能够在不必使用静态方法的情况下修改变量是有利的。 Swift通过.语法表示这意味着什么。缺点是它不知道哪些方法对变量起作用以及哪些方法对值有影响。

答案 4 :(得分:5)

我发现:当且仅当操作有可能改变数组的长度 时,数组将是引用的 的可变副本。在上一个示例中,f[0..2]使用多个索引进行索引,操作可能会更改其长度(可能是不允许重复),因此它会被复制。

var e = [1, 2, 3]
var f = e
e[0..2] = [4, 5]
e // 4,5,3
f // 1,2,3


var e1 = [1, 2, 3]
var f1 = e1

e1[0] = 4
e1[1] = 5

e1 //  - 4,5,3
f1 // - 4,5,3

答案 5 :(得分:5)

对我来说,如果你先用变量替换常量,那就更有意义了:

a[i] = 42            // (1)
e[i..j] = [4, 5]     // (2)

第一行永远不需要更改a的大小。特别是,它永远不需要进行任何内存分配。无论i的值如何,这都是轻量级操作。如果您认为引擎盖下a是一个指针,它可以是一个常量指针。

第二行可能要复杂得多。根据{{​​1}}和i的值,您可能需要进行内存管理。如果你想象j是一个指向数组内容的指针,你就不能再认为它是一个常量指针;您可能需要分配一个新的内存块,将数据从旧内存块复制到新内存块,然后更改指针。

语言设计者似乎试图保持(1)尽可能轻量级。因为(2)可能涉及复制,他们已经采用了解决方案,它总是像你复印一样。

这很复杂,但我很高兴他们没有让它更复杂,例如特殊情况,如“如果在(2)i和j是编译时常量,编译器可以推断e的大小不会改变,那么我们不复制”


最后,基于我对Swift语言设计原则的理解,我认为一般规则是:

  • 默认情况下始终使用常量(e),并且不会有任何重大意外。
  • 仅在绝对必要的情况下使用变量(let),并且在这些情况下要小心,因为会出现意外[这里:某些但不是所有情况下阵列的奇怪隐式副本]。

答案 6 :(得分:4)

Delphi的字符串和数组具有完全相同的“功能”。当你查看实现时,它是有道理的。

每个变量都是指向动态内存的指针。该内存包含一个引用计数,后跟数组中的数据。因此,您可以轻松更改数组中的值,而无需复制整个数组或更改任何指针。如果要调整阵列大小,则必须分配更多内存。在这种情况下,当前变量将指向新分配的内存。但是你不能轻易地追踪指向原始数组的所有其他变量,所以你可以不管它们。

当然,要实现更一致的实施并不困难。如果您希望所有变量都能看到调整大小,请执行以下操作: 每个变量都是指向存储在动态内存中的容器的指针。容器只包含两个东西,一个引用计数和指向实际数组数据的指针。阵列数据存储在单独的动态存储器块中。现在只有一个指向数组数据的指针,因此您可以轻松调整该数据的大小,并且所有变量都会看到更改。

答案 7 :(得分:4)

许多Swift早期采用者都抱怨这种容易出错的数组语义,Chris Lattner已经写过,数组语义已被修改以提供完整的值语义(Apple Developer link for those who have an account)。我们必须至少等待下一个测试版看看这究竟意味着什么。

答案 8 :(得分:0)

我为此使用.copy()。

    var a = [1, 2, 3]
    var b = a.copy()
     a[1] = 42 

答案 9 :(得分:0)

在更高的Swift版本中,数组行为是否有所改变?我只是运行您的示例:

var a = [1, 2, 3]
var b = a
a[1] = 42
a
b

我的结果是[1、42、3]和[1、2、3]