是否有一个Swift数组赋值不一致的原因(既不是一个引用也不是深层复制)?

我正在阅读文档,我经常在语言的一些devise决定上摇头。 但真正让我困惑的是数组是如何处理的。

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

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

这里ab都是[1, 42, 3] ,我可以接受。 数组被引用 – OK!

现在看到这个例子:

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

c[1, 2, 3, 42] 1,2,3,42 [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] ,这很酷。 有一个多索引replace是很好的,但即使长度没有改变,仍然没有看到改变。

所以总结一下,如果你改变1个元素,常见的数组引用就会发生变化,但是如果你改变了多个元素或者附加了一个元素,就会产生一个副本。

这对我来说似乎是一个非常糟糕的devise。 我在想这个吗? 有没有理由我不明白为什么数组应该这样做?

编辑 :数组已经改变,现在有价值的语义。 更多的理智!

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


这是出于性能的原因。 基本上,他们尽量避免复制数组(并声称“类似C的性能”)。 引用语言书 :

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

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

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

从Swift语言的官方文档 :

请注意,当您使用下标语法设置新值时,不会复制数组,因为使用下标语法设置单个值不具有更改数组长度的潜力。 但是,如果将一个新项目追加到数组中,则可以修改数组的长度 。 这会提示Swift在添加新值的地方创build数组新副本 。 此后,a是一个单独的,独立的arrays副本…..

阅读本文档中“ 数组的赋值和复制行为”一节。 你会发现, 当你做了replace数组中的项目的范围,然后该数组为自己的所有项目的副本。

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


老答案:

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

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

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

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

在C中,结构不过是variables的聚合。 应用. 到结构types的variables将访问存储在结构内的variables。 指向对象的指针不包含variables的集合,但是标识它们。 如果有一个标识结构的指针,则可以使用->运算符来访问存储在指针标识的结构中的variables。

在C ++中,结构和类不仅可以聚合variables,还可以附加代码。 使用. 调用一个方法会对一个variables要求该方法作用于variables本身的内容 ; 使用->标识一个对象的variables将要求该方法作用于由variables标识的对象。

在Java中,所有自定义variablestypes只是简单地标识对象,并且在variables上调用一个方法会告诉方法variables标识了哪个对象。 variables不能直接保存任何types的复合数据types,也没有任何方法可以访问被调用的variables。 这些限制虽然在语义上受到限制,但极大地简化了运行时间,并便于字节码validation; 这样的简化在市场对这些问题敏感的时候减less了Java的资源开销,从而帮助它在市场上获得了牵引力。 他们也意味着没有必要有一个相当于标志的标志. 用于C或C ++。 尽pipeJava可以像C和C ++一样使用,但创build者select使用单字符. 因为不需要其他用途。

在C#和其他.NET语言中,variables可以标识对象或直接保存复合数据types。 在复合数据types的variables上使用时. 作用于variables的内容 ; 当在一个引用types的variables上使用时. 作用于它所确定的对象。 对于某些types的操作,语义上的区别不是特别重要,但是对于其他types而言。 最成问题的情况是在一个只读variables上调用一个组合数据types的方法,这个方法会修改被调用的variables。 如果尝试在只读值或variables上调用方法,编译器通常会复制该variables,让该方法作用于该variables,然后放弃该variables。 对于只读取variables的方法而言,这通常是安全的,但对于写入方法的方法来说并不安全。 不幸的是,.does还没有任何方法可以指出哪些方法可以安全地用于这种replace,哪些方法不能。

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

如果有一个时间机器,并回到创buildC#和/或Swift,可以通过语言的使用来追溯地避免围绕这些问题的困惑.->令牌的方式更接近于C ++的使用。 这两种聚合和参考types的方法可以使用. 对它们被调用的variables采取行动,并且->对一个 (对于复合体)或对其进行识别的事物(对于参考types)采取行动。 但是,这两种语言都不是这样devise的。

在C#中,修改调用variables的方法的一般做法是将variables作为parameter passing给方法。 这样调用Array.Resize(ref someArray, 23);someArray标识一个20个元素的数组时,会导致someArray标识一个23个元素的新数组,而不影响原始数组。 ref的使用清楚地表明该方法应该被期望修改被调用的variables。 在许多情况下,能够修改variables而无需使用静态方法是有利的。 Swift地址意味着通过使用. 句法。 缺点是,它弄清楚什么方法对variables起作用,什么方法对价值起作用。

对我来说,如果你首先用variablesreplace你的常量,这会更有意义:

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

第一行永远不需要改变a的大小。 特别是,它永远不需要做任何内存分配。 不pipei的价值如何,这是一个轻量级的操作。 如果你想象在引擎盖下是一个指针,它可以是一个常量指针。

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

看起来,语言devise者试图保持(1)尽可能的轻量级。 由于(2)可能涉及复制,他们已经采取了解决scheme,它总是就像你做了副本。

这很复杂,但我很高兴他们没有使它变得更复杂,例如“如果在(2)中,如果i和j是编译时常量,编译器可以推断e的大小不会改变,然后我们不复制“


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

  • 默认情况下,常常使用常量( let ),并且不会有任何重大的意外。
  • 只有在绝对必要的情况下才使用variables( var ),在这种情况下要小心谨慎,因为会有惊喜[在这里:在一些但并非所有的情况下奇怪的数组隐式拷贝]。

我发现的是: 当且仅当操作有可能改变数组的长度时,数组才是被引用数组的可变副本。 在你最后的例子中, 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 

delphi的string和数组有完全相同的“function”。 当你看执行,这是有道理的。

每个variables都是一个指向dynamic内存的指针。 该内存包含引用计数,后跟数组中的数据。 所以你可以很容易地改变数组中的值而不需要复制整个数组或者改变指针。 如果你想调整数组的大小,你必须分配更多的内存。 在这种情况下,当前variables将指向新分配的内存。 但是你不能轻易地追踪指向原始数组的所有其他variables,所以你不要pipe它们。

当然,要做出更一致的实施并不困难。 如果你想要所有的variables看到一个resize,做到这一点:每个variables是一个指向存储在dynamic内存中的容器的指针。 容器只包含两件事情,一个引用计数和指向实际数组数据的指针。 arrays数据存储在一个独立的dynamic存储器块中。 现在只有一个指向数组数据的指针,所以你可以很容易地调整它的大小,所有的variables都会看到变化。

很多Swift早期使用者都抱怨这种容易出错的数组语义,Chris Lattner写到数组语义已经被修改为提供完整的值语义( Apple Developer链接给那些有账户的人 )。 我们将不得不等待下一个testing版,看看这是什么意思。

我使用.copy()。

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