Swift数组分配不一致(既不是引用也不是深拷贝)是有原因的吗?


216

我正在阅读文档,并且在语言的一些设计决策中不断摇头。但是,真正让我感到困惑的是如何处理数组。

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

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]BUT 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即使长度没有变化,STILL也看不到变化。

综上所述,如果更改1个元素,则对数组的公共引用会发生更改,但是如果更改多个元素或附加项,则会创建一个副本。

对我来说,这似乎是一个非常糟糕的设计。我是这样想的吗?有没有我看不出为什么数组应该这样工作的原因?

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


95
出于记录,我认为这个问题不应该解决。Swift是一门新语言,因此在大家学习的过程中,将会有一段时间这样的问题。我觉得这个问题非常有趣,希望有人在辩方中提出令人信服的案子。
乔尔·伯杰

4
@Joel Fine,向程序员询问,Stack Overflow是针对特定的,不受限制的编程问题。
bjb568 2014年

21
@ bjb568:不过,这不是意见。这个问题应以事实回答。如果某个Swift开发人员来回答“我们在X,Y和Z上这样做的话”,那么这就是事实。您可能不同意X,Y和Z,但是如果对X,Y和Z做出决定,那么那只是该语言设计的历史事实。就像当我问为什么std::shared_ptr没有非原子版本时,有一个基于事实而不是观点的答案(事实是委员会考虑了它,但出于各种原因而不希望它)。
Cornstalks 2014年

7
@JasonMArcher:只有最后一段是基于意见的(是的,也许应该删除)。问题的实际标题(我将其视为实际问题本身)可以用事实回答。这里阵列设计的工作,他们的工作方式的原因。
2014年

7
是的,就像API-Beast所说的那样,通常称为“半复制辅助语言设计”。
R. Martinho Fernandes

Answers:


109

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


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

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

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

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


61
我发现您既取消共享又在设计中复制了一个大红色标记。
Cthutu 2014年

9
这是对的。一位工程师向我描述,对于语言设计而言,这是不可取的,他们希望在即将到来的Swift更新中“解决”这一问题。用雷达投票。
Erik Kerber 2014年

2
就像Linux子进程内存管理中的写时复制(COW)一样,对吗?也许我们可以称其为按长度复制(COLA)。我认为这是一个积极的设计。
Justhalf 2014年

3
@justhalf我可以预料到一堆迷茫的新手,因此询问为什么/不共享他们的数组(只是不太清楚)。
John Dvorak 2014年

11
@justhalf:无论如何,COW在现代世界中都是一种悲观,其次,COW是仅用于实现的技术,而这种COLA的东西会导致完全随机的共享和共享。
小狗

25

从Swift语言的官方文档中

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

阅读本文档中的整个部分“阵列的分配和复制行为”。您会发现,当确实替换阵列中的一系列项目时,阵列会为所有项目获取其自身的副本。


4
谢谢。我在问题中含糊地提及了该案文。但是我展示了一个示例,其中更改下标范围不会更改长度,并且仍然可以复制。因此,如果您不想复制,则必须一次更改一个元素。
Cthutu 2014年

21

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]

嗯,对我来说,那个unshare()方法是不确定的。
Hlung 2014年

1
@Hlung在beta 3中已将其删除,我已经更新了答案。
Pascal

12

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

在C语言中,结构无非就是变量的集合。将._ 应用于结构类型的变量将访问存储在结构中的变量。指向对象的指针不保存变量的集合,而是识别它们。如果具有标识结构的指针,->则可使用运算符访问指针所标识的结构中存储的变量。

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

在Java中,所有自定义变量类型都仅标识对象,对变量调用方法将告诉该方法该变量标识的对象。变量不能直接保存任何种类的复合数据类型,也没有任何方法可以访问方法对其进行调用的变量。这些限制尽管在语义上受到限制,但可以大大简化运行时间并简化字节码验证。在市场对此类问题敏感的时候,这种简化减少了Java的资源开销,从而帮助Java赢得了市场的关注。它们还意味着不需要等同.于C或C ++中使用的令牌。尽管Java可能->以与C和C ++相同的方式使用,但创建者选择使用单字符. 因为它不需要用于任何其他目的。

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

在Swift中,集合上的方法可以明确表示它们是否会修改对其调用的变量,并且编译器将禁止对只读变量使用mutation方法(而不是让它们对变量的临时副本进行突变,然后被丢弃)。由于存在这种区别,因此.在Swift中使用令牌调用方法来修改对其进行调用的变量的方法要比.NET中的方法安全得多。不幸的是,使用相同的.令牌用于作用于变量所标识的外部对象的事实意味着仍然存在混淆的可能性。

如果拥有一台时光机,然后回到C#和/或Swift的创建,则可以通过使语言以更接近C ++的用法来使用.->令牌,从而追溯避免围绕此类问题的许多混乱。聚集和引用类型的方法都可以.作用于对其调用的变量,并->作用于(对于复合物)或由此标识的事物(对于引用类型)。但是,两种语言都不是那样设计的。

在C#中,方法修改在其上被调用的变量的通常做法是将变量作为ref参数传递给方法。因此,调用Array.Resize(ref someArray, 23);when someArray标识20个元素的数组将导致someArray标识23个元素的新数组,而不影响原始数组。使用ref可以清楚地看到,应该期望该方法修改在其上调用它的变量。在许多情况下,无需使用静态方法即可修改变量是有利的。Swift地址意味着使用.语法。缺点是,它对哪些方法作用于变量以及哪些方法作用于值缺乏澄清。


5

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

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

第一行无需更改的大小a。特别是,它永远不需要进行任何内存分配。无论的值如何i,这都是轻量级的操作。如果您想象在幕后a是一个指针,那么它可以是一个常量指针。

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

语言设计者似乎已尝试保持(1)尽可能轻巧。由于(2)可能仍涉及复制,因此他们采取的解决方案始终像您进行复制一样工作。

这很复杂,但是我很高兴他们没有使它变得更加复杂,例如“(2)中的i和j是编译时常量,并且编译器可以推断出e的大小不会变进行更改,那么我们就不会复制”


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

  • let默认情况下,始终在所有位置使用常量(),不会有任何重大惊喜。
  • var仅在绝对必要时才使用变量(),在这种情况下要多加注意,因为会出乎意料[此处:在某些情况下但并非在所有情况下,数组的隐式隐式副本]。

5

我发现的是:当且仅当该操作有可能更改数组的length时,该数组才是所引用数组的可变副本。在最后一个示例中,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

8
“当长度改变时”我可以理解,如果长度改变了,它将被复制,但是结合上面的引用,我认为这是一个非常令人担忧的“功能”,我认为很多人都会犯错
Joel Berger

25
仅仅因为一种语言是新的,并不意味着它包含了明显的内部矛盾。
2014年

它已在beta 3中得到“修复”,var数组现在完全可变,let数组完全不可变。
Pascal

4

Delphi的字符串和数组具有完全相同的“功能”。当您查看实现时,这很有意义。

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

当然,进行更一致的实现并不难。如果希望所有变量都具有调整大小,请执行以下操作:每个变量都是指向动态内存中存储的容器的指针。容器恰好容纳了两件事,即引用计数和指向实际数组数据的指针。阵列数据存储在单独的动态内存块中。现在只有一个指向数组数据的指针,因此您可以轻松调整其大小,所有变量都将看到更改。



0

我为此使用.copy()。

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

1
运行您的代码时,我收到“类型'[Int]'的值没有成员'copy'”
jreft56

0

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

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.