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

Cth*_*utu 216 arrays swift

我正在阅读文档,我不断地在语言的某些设计决策中摇头.但令我困惑的是如何处理数组.

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

var a = [1, 2, 3]
var b = a
a[1] = 42
a
b
Run Code Online (Sandbox Code Playgroud)

这里ab都是[1, 42, 3],我可以接受.数组被引用 - 好的!

现在看这个例子:

var c = [1, 2, 3]
var d = c
c.append(42)
c
d
Run Code Online (Sandbox Code Playgroud)

c[1, 2, 3, 42]BUT d[1, 2, 3].也就是说,d看到了最后一个例子中的变化,但是在这个例子中看不到它.文档说这是因为长度发生了变化.

现在,这个怎么样:

var e = [1, 2, 3]
var f = e
e[0..2] = [4, 5]
e
f
Run Code Online (Sandbox Code Playgroud)

e是的[4, 5, 3],这很酷.有一个多索引替换很好,但f即使长度没有改变,STILL也看不到变化.

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

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

编辑:数组已经改变,现在有了价值语义.更加理智!

Luk*_*kas 109

请注意,在Xcode beta 3版本(博客文章)中更改了数组语义和语法,因此该问题不再适用.以下答案适用于beta 2:


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

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

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

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

  • 我发现你有两个取消共享并在设计中复制一个大红旗的事实. (61认同)
  • @justhalf:COW在现代世界中是一种悲观,其次,COW是一种仅实现技术,而这种COLA的东西导致完全随机共享和取消共享. (11认同)
  • 这是对的.一位工程师向我描述,对于语言设计而言,这是不可取的,他们希望在即将发布的Swift更新中"修复".投票与雷达. (9认同)
  • @justhalf我可以预测到一堆混乱的新手来到SO并询问为什么他们的阵列是/不共享的(只是以不太清楚的方式). (3认同)
  • 它就像Linux子进程内存管理中的copy-on-write(COW),对吧?也许我们可以称之为复制长度改变(COLA).我认为这是一个积极的设计. (2认同)
  • @justhalf,设计令人作呕.就个人而言,如果我想要一份副本,我想明确说明要制作一个新副本.在一个翻转的音符 - 无论质量不同意你,它仍然不暗示你本身是错的.然而,在这种情况下,设计对我来说毫无意义 - 这太荒谬了. (2认同)

iPa*_*tel 25

从Swift语言的官方文档:

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

阅读本文档中的整个"数组的赋值和复制行为"一节.您会发现当您更换数组中的一系列项目时,数组会为所有项目获取自身的副本.

  • 谢谢.在我的问题中,我含糊地提到了那个文字.但我展示了一个例子,更改下标范围并没有改变长度,它仍然被复制.因此,如果您不想要副本,则必须一次更改一个元素. (4认同)

Pas*_*cal 20

Xcode 6 beta 3的行为发生了变化.数组不再是引用类型,并且具有写时复制机制,这意味着只要从一个或另一个变量更改数组的内容,就会复制数组,并且只复制数组一份副本将被更改.


老答案:

正如其他人所指出的那样,Swift尽可能避免复制数组,包括一次更改单个索引的值.

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

var a = [1, 2, 3]
var b = a
b.unshare()
a[1] = 42
a               // [1, 42, 3]
b               // [1, 2, 3]
Run Code Online (Sandbox Code Playgroud)


sup*_*cat 12

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

在C中,结构只不过是变量的集合.将.结构类型应用于变量将访问存储在结构中的变量.对象指针不持有的变量聚合,但识别它们.如果一个指针具有标识结构的指针,则->操作符可用于访问存储在由指针标识的结构内的变量.

在C++中,结构和类不仅聚合变量,还可以将代码附加到它们.使用.调用方法将一个变量问的方法来行事时变量本身的内容 ; 使用->在其上标识对象将询问该方法在对象起作用的可变标识由可变.

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

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

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

如果有一台时间机器并且回到创建C#和/或Swift,那么可以通过让语言以更接近C++使用的方式使用.->令牌来追溯地避免围绕这些问题的大部分混淆.聚合和引用类型的方法可以用于.对调用它们的变量进行操作,并对(对于复合)或由此识别的事物(对于引用类型)->起作用.然而,这两种语言都不是这样设计的.

在C#中,修改调用它的变量的方法的通常做法是将变量作为ref参数传递给方法.因此,Array.Resize(ref someArray, 23);someArray识别20个元素的数组时调用将导致someArray识别23个元素的新数组,而不会影响原始数组.使用ref清楚地表明该方法应该被修改调用它的变量.在许多情况下,能够在不必使用静态方法的情况下修改变量是有利的.Swift通过使用.语法来表示.缺点是它失去了关于哪些方法对变量起作用以及哪些方法对值起作用的澄清.


Kum*_* KL 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
Run Code Online (Sandbox Code Playgroud)

  • 仅仅因为语言是新的并不意味着它包含明显的内部矛盾是可以的. (25认同)
  • "长度已经改变了"我知道如果长度改变会被复制,但结合上面的引用,我认为这是一个非常令人担忧的"特征",我认为很多人会出错 (8认同)

Juk*_*ela 5

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

a[i] = 42            // (1)
e[i..j] = [4, 5]     // (2)
Run Code Online (Sandbox Code Playgroud)

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

第二行可能要复杂得多.根据的价值观ij,你可能需要做内存管理.如果你想象这e是一个指向数组内容的指针,你就不能再认为它是一个常量指针; 您可能需要分配新的内存块,将数据从旧内存块复制到新内存块,然后更改指针.

似乎语言设计者试图保持(1)尽可能轻量级.因为(2)可能涉及复制,他们已经采取了解决方案,它总是表现得好像你做了一个副本.

这很复杂,但我很高兴他们没有让它变得更复杂,例如"if in(2)i和j是编译时常量,编译器可以推断出e的大小不会发生改变,然后我们不复制".


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

  • let默认情况下总是在任何地方使用常量(),并且不会有任何重大意外.
  • var只有在绝对必要的情况下才使用变量(),并且在这些情况下要小心,因为会出现意外[这里:在一些但不是所有情况下奇怪的数组隐式副本].