首页 文章

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

提问于
浏览
215

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

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

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 在最后一个例子中看到了变化,但由于长度发生了变化,因此没有't see it in this one. The documentation says that' .

现在,这个怎么样:

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

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

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

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

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

9 回答

  • 0

    请注意 array semantics and syntax was changed in Xcode beta 3 versionblog post),因此问题不再适用 . 以下答案适用于beta 2:


    这是出于性能原因 . 基本上,他们尽量避免复制数组(并声称"C-like performance") . 引用语言book

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

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

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

  • 20

    From the official documentation of the Swift language

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

    阅读本文档中的整个部分 Assignment and Copy Behavior for Arrays . 你会发现 replace a range of items in the array then the array takes a copy of itself for all items.

  • 25

    Xcode 6 beta 3的行为已经改变 . 数组不再是引用类型,并且具有 copy-on-write 机制,这意味着只要您从一个或另一个变量更改数组的内容,就会复制该数组,并且只有一个副本将被复制改变 .


    老答案:

    正如其他人所指出的,如果可能的话,Swift会尝试 avoid copying 数组,包括一次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]
    
  • 12

    该行为与.NET中的 Array.Resize 方法非常相似 . 要了解发生了什么,查看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 参数传递给方法 . 因此,当 someArray 标识一个包含20个元素的数组时,调用 Array.Resize(ref someArray, 23); 将导致 someArray 识别出一个包含23个元素的新数组,而不会影响原始数组 . 使用 ref 表明应该期望该方法修改调用它的变量 . 在许多情况下,能够在不必使用静态方法的情况下修改变量是有利的 . Swift地址表示使用 . 语法 . 缺点是它失去了关于哪些方法对变量起作用以及哪些方法对值起作用的澄清 .

  • 5

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

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

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

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

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

    这很复杂,但我很高兴他们没有让它更复杂,例如特殊情况如"if in (2) i and j are compile-time constants and the compiler can infer that the size of e is not going to change, then we do not copy" .


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

    • 默认情况下始终使用常量( let ),并且不会有任何重大意外 .

    • 仅在绝对必要的情况下使用变量( var ),并且在这些情况下要小心,因为会出现意外[这里:某些但不是所有情况下阵列的奇怪隐式副本] .

  • 108

    我发现的是:数组将是引用的 if and only if the operation has the potential to change the array's 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
    
  • 5

    德尔福字符串和数组具有完全相同的“特征” . 当你看到实现时,它是有道理的 .

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

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

  • 4

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

  • 4

    我为此使用.copy() .

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

相关问题